Browse Source

Merge branch 'Rapptz:master' into voice-messages

pull/10230/head
blord0 2 weeks ago
committed by GitHub
parent
commit
aa84304fa8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .github/workflows/lint.yml
  2. 7
      discord/__init__.py
  3. 4
      discord/__main__.py
  4. 235
      discord/abc.py
  5. 40
      discord/activity.py
  6. 12
      discord/app_commands/checks.py
  7. 61
      discord/app_commands/commands.py
  8. 104
      discord/app_commands/models.py
  9. 6
      discord/app_commands/namespace.py
  10. 18
      discord/app_commands/translator.py
  11. 39
      discord/app_commands/tree.py
  12. 2
      discord/appinfo.py
  13. 8
      discord/asset.py
  14. 37
      discord/audit_logs.py
  15. 26
      discord/automod.py
  16. 9
      discord/backoff.py
  17. 145
      discord/channel.py
  18. 243
      discord/client.py
  19. 782
      discord/components.py
  20. 3
      discord/embeds.py
  21. 70
      discord/enums.py
  22. 2
      discord/errors.py
  23. 11
      discord/ext/commands/_types.py
  24. 57
      discord/ext/commands/bot.py
  25. 19
      discord/ext/commands/cog.py
  26. 101
      discord/ext/commands/context.py
  27. 6
      discord/ext/commands/converter.py
  28. 4
      discord/ext/commands/cooldowns.py
  29. 138
      discord/ext/commands/core.py
  30. 2
      discord/ext/commands/errors.py
  31. 2
      discord/ext/commands/flags.py
  32. 53
      discord/ext/commands/help.py
  33. 65
      discord/ext/commands/hybrid.py
  34. 3
      discord/ext/commands/parameters.py
  35. 32
      discord/ext/commands/view.py
  36. 14
      discord/file.py
  37. 62
      discord/flags.py
  38. 8
      discord/gateway.py
  39. 143
      discord/guild.py
  40. 98
      discord/http.py
  41. 100
      discord/interactions.py
  42. 7
      discord/invite.py
  43. 180
      discord/message.py
  44. 369
      discord/onboarding.py
  45. 12
      discord/opus.py
  46. 6
      discord/partial_emoji.py
  47. 108
      discord/permissions.py
  48. 15
      discord/player.py
  49. 2
      discord/poll.py
  50. 1
      discord/presences.py
  51. 2
      discord/primary_guild.py
  52. 2
      discord/reaction.py
  53. 18
      discord/role.py
  54. 15
      discord/scheduled_event.py
  55. 10
      discord/shard.py
  56. 1
      discord/sku.py
  57. 4
      discord/soundboard.py
  58. 21
      discord/state.py
  59. 6
      discord/threads.py
  60. 6
      discord/types/activity.py
  61. 33
      discord/types/audit_log.py
  62. 3
      discord/types/automod.py
  63. 1
      discord/types/channel.py
  64. 2
      discord/types/command.py
  65. 104
      discord/types/components.py
  66. 1
      discord/types/gateway.py
  67. 8
      discord/types/guild.py
  68. 25
      discord/types/interactions.py
  69. 6
      discord/types/invite.py
  70. 16
      discord/types/message.py
  71. 72
      discord/types/onboarding.py
  72. 9
      discord/types/scheduled_event.py
  73. 1
      discord/types/threads.py
  74. 3
      discord/types/webhook.py
  75. 9
      discord/ui/__init__.py
  76. 588
      discord/ui/action_row.py
  77. 67
      discord/ui/button.py
  78. 375
      discord/ui/container.py
  79. 15
      discord/ui/dynamic.py
  80. 159
      discord/ui/file.py
  81. 77
      discord/ui/item.py
  82. 140
      discord/ui/label.py
  83. 263
      discord/ui/media_gallery.py
  84. 35
      discord/ui/modal.py
  85. 261
      discord/ui/section.py
  86. 132
      discord/ui/select.py
  87. 125
      discord/ui/separator.py
  88. 90
      discord/ui/text_display.py
  89. 27
      discord/ui/text_input.py
  90. 144
      discord/ui/thumbnail.py
  91. 582
      discord/ui/view.py
  92. 4
      discord/user.py
  93. 84
      discord/utils.py
  94. 6
      discord/voice_client.py
  95. 2
      discord/voice_state.py
  96. 102
      discord/webhook/async_.py
  97. 85
      discord/webhook/sync.py
  98. 4
      discord/widget.py
  99. 305
      docs/api.rst
  100. 79
      docs/faq.rst

6
.github/workflows/lint.yml

@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
id: install-deps
run: |
python -m pip install --upgrade pip setuptools wheel black==22.6 requests
python -m pip install --upgrade pip setuptools wheel ruff==0.12 requests
pip install -U -r requirements.txt
- name: Setup node.js
@ -42,7 +42,7 @@ jobs:
warnings: false
no-comments: ${{ matrix.python-version != '3.x' }}
- name: Run black
- name: Run ruff
if: ${{ always() && steps.install-deps.outcome == 'success' }}
run: |
black --check discord examples
ruff format --check discord examples

7
discord/__init__.py

@ -13,7 +13,7 @@ __title__ = 'discord'
__author__ = 'Rapptz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz'
__version__ = '2.6.0a'
__version__ = '2.7.0a'
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
@ -74,17 +74,18 @@ from .soundboard import *
from .subscription import *
from .presences import *
from .primary_guild import *
from .onboarding import *
class VersionInfo(NamedTuple):
major: int
minor: int
micro: int
releaselevel: Literal["alpha", "beta", "candidate", "final"]
releaselevel: Literal['alpha', 'beta', 'candidate', 'final']
serial: int
version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0)
version_info: VersionInfo = VersionInfo(major=2, minor=7, micro=0, releaselevel='alpha', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler())

4
discord/__main__.py

@ -133,7 +133,7 @@ async def setup(bot):
await bot.add_cog({name}(bot))
'''
_cog_extras = '''
_cog_extras = """
async def cog_load(self):
# loading logic goes here
pass
@ -170,7 +170,7 @@ _cog_extras = '''
# called after a command is called here
pass
'''
"""
# certain file names and directory names are forbidden

235
discord/abc.py

@ -34,8 +34,10 @@ from typing import (
AsyncIterator,
Callable,
Dict,
Generator,
Iterable,
List,
Literal,
Optional,
TYPE_CHECKING,
Protocol,
@ -61,6 +63,7 @@ from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem
from . import utils
from .flags import InviteFlags
import warnings
__all__ = (
'Snowflake',
@ -74,7 +77,7 @@ __all__ = (
T = TypeVar('T', bound=VoiceProtocol)
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from .client import Client
from .user import ClientUser
@ -96,7 +99,7 @@ if TYPE_CHECKING:
)
from .poll import Poll
from .threads import Thread
from .ui.view import View
from .ui.view import BaseView, View, LayoutView
from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload,
Channel as ChannelPayload,
@ -109,10 +112,16 @@ if TYPE_CHECKING:
from .types.snowflake import (
SnowflakeList,
)
from .permissions import _PermissionOverwriteKwargs
PartialMessageableChannel = Union[TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable]
MessageableChannel = Union[PartialMessageableChannel, GroupChannel]
SnowflakeTime = Union["Snowflake", datetime]
SnowflakeTime = Union['Snowflake', datetime]
class PinnedMessage(Message):
pinned_at: datetime
pinned: Literal[True]
MISSING = utils.MISSING
@ -125,6 +134,26 @@ class _Undefined:
_undefined: Any = _Undefined()
class _PinsIterator:
def __init__(self, iterator: AsyncIterator[PinnedMessage]) -> None:
self.__iterator: AsyncIterator[PinnedMessage] = iterator
def __await__(self) -> Generator[Any, None, List[PinnedMessage]]:
warnings.warn(
'`await <channel>.pins()` is deprecated; use `async for message in <channel>.pins()` instead.',
DeprecationWarning,
stacklevel=2,
)
async def gather() -> List[PinnedMessage]:
return [msg async for msg in self.__iterator]
return gather().__await__()
def __aiter__(self) -> AsyncIterator[PinnedMessage]:
return self.__iterator
async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None):
for m in messages:
try:
@ -394,8 +423,7 @@ class GuildChannel:
if TYPE_CHECKING:
def __init__(self, *, state: ConnectionState, guild: Guild, data: GuildChannelPayload):
...
def __init__(self, *, state: ConnectionState, guild: Guild, data: GuildChannelPayload): ...
def __str__(self) -> str:
return self.name
@ -765,7 +793,6 @@ class GuildChannel:
default = self.guild.default_role
if default is None:
if self._state.self_id == obj.id:
return Permissions._user_installed_permissions(in_guild=True)
else:
@ -878,8 +905,7 @@ class GuildChannel:
*,
overwrite: Optional[Union[PermissionOverwrite, _Undefined]] = ...,
reason: Optional[str] = ...,
) -> None:
...
) -> None: ...
@overload
async def set_permissions(
@ -887,9 +913,8 @@ class GuildChannel:
target: Union[Member, Role],
*,
reason: Optional[str] = ...,
**permissions: Optional[bool],
) -> None:
...
**permissions: Unpack[_PermissionOverwriteKwargs],
) -> None: ...
async def set_permissions(
self,
@ -897,7 +922,7 @@ class GuildChannel:
*,
overwrite: Any = _undefined,
reason: Optional[str] = None,
**permissions: Optional[bool],
**permissions: Unpack[_PermissionOverwriteKwargs],
) -> None:
r"""|coro|
@ -1080,8 +1105,7 @@ class GuildChannel:
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: Optional[str] = MISSING,
) -> None:
...
) -> None: ...
@overload
async def move(
@ -1092,8 +1116,7 @@ class GuildChannel:
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
) -> None: ...
@overload
async def move(
@ -1104,8 +1127,7 @@ class GuildChannel:
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
) -> None: ...
@overload
async def move(
@ -1116,8 +1138,7 @@ class GuildChannel:
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
) -> None: ...
async def move(self, **kwargs: Any) -> None:
"""|coro|
@ -1386,6 +1407,36 @@ class Messageable:
async def _get_channel(self) -> MessageableChannel:
raise NotImplementedError
@overload
async def send(
self,
*,
file: File = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def send(
self,
*,
files: Sequence[File] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def send(
self,
@ -1404,8 +1455,7 @@ class Messageable:
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -1425,8 +1475,7 @@ class Messageable:
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -1446,8 +1495,7 @@ class Messageable:
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -1467,8 +1515,7 @@ class Messageable:
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
async def send(
self,
@ -1485,7 +1532,7 @@ class Messageable:
allowed_mentions: Optional[AllowedMentions] = None,
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
mention_author: Optional[bool] = None,
view: Optional[View] = None,
view: Optional[BaseView] = None,
suppress_embeds: bool = False,
silent: bool = False,
poll: Optional[Poll] = None,
@ -1558,7 +1605,7 @@ class Messageable:
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
.. versionadded:: 1.6
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
A Discord UI View to add to the message.
.. versionadded:: 2.0
@ -1665,7 +1712,7 @@ class Messageable:
data = await state.http.send_message(channel.id, params=params)
ret = state.create_message(channel=channel, data=data)
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
state.store_view(view, ret.id)
if poll:
@ -1731,17 +1778,119 @@ class Messageable:
data = await self._state.http.get_message(channel.id, id)
return self._state.create_message(channel=channel, data=data)
async def pins(self) -> List[Message]:
"""|coro|
async def __pins(
self,
*,
limit: Optional[int] = 50,
before: Optional[SnowflakeTime] = None,
oldest_first: bool = False,
) -> AsyncIterator[PinnedMessage]:
channel = await self._get_channel()
state = self._state
max_limit: int = 50
time: Optional[str] = (
(before if isinstance(before, datetime) else utils.snowflake_time(before.id)).isoformat()
if before is not None
else None
)
Retrieves all messages that are currently pinned in the channel.
while True:
retrieve = max_limit if limit is None else min(limit, max_limit)
if retrieve < 1:
break
data = await self._state.http.pins_from(
channel_id=channel.id,
limit=retrieve,
before=time,
)
items = data and data['items']
if items:
if limit is not None:
limit -= len(items)
time = items[-1]['pinned_at']
# Terminate loop on next iteration; there's no data left after this
if len(items) < max_limit or not data['has_more']:
limit = 0
if oldest_first:
items = reversed(items)
count = 0
for count, m in enumerate(items, start=1):
message: Message = state.create_message(channel=channel, data=m['message'])
message._pinned_at = utils.parse_time(m['pinned_at'])
yield message # pyright: ignore[reportReturnType]
if count < max_limit:
break
def pins(
self,
*,
limit: Optional[int] = 50,
before: Optional[SnowflakeTime] = None,
oldest_first: bool = False,
) -> _PinsIterator:
"""Retrieves an :term:`asynchronous iterator` of the pinned messages in the channel.
You must have :attr:`~discord.Permissions.view_channel` and
:attr:`~discord.Permissions.read_message_history` in order to use this.
.. versionchanged:: 2.6
Due to a change in Discord's API, this now returns a paginated iterator instead of a list.
For backwards compatibility, you can still retrieve a list of pinned messages by
using ``await`` on the returned object. This is however deprecated.
.. note::
Due to a limitation with the Discord API, the :class:`.Message`
objects returned by this method do not contain complete
object returned by this method does not contain complete
:attr:`.Message.reactions` data.
Examples
---------
Usage ::
counter = 0
async for message in channel.pins(limit=250):
counter += 1
Flattening into a list: ::
messages = [message async for message in channel.pins(limit=50)]
# messages is now a list of Message...
All parameters are optional.
Parameters
-----------
limit: Optional[int]
The number of pinned messages to retrieve. If ``None``, it retrieves
every pinned message in the channel. Note, however, that this would
make it a slow operation.
Defaults to ``50``.
.. versionadded:: 2.6
before: Optional[Union[:class:`datetime.datetime`, :class:`.abc.Snowflake`]]
Retrieve pinned messages before this time or snowflake.
If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
.. versionadded:: 2.6
oldest_first: :class:`bool`
If set to ``True``, return messages in oldest pin->newest pin order.
Defaults to ``False``.
.. versionadded:: 2.6
Raises
-------
~discord.Forbidden
@ -1749,16 +1898,12 @@ class Messageable:
~discord.HTTPException
Retrieving the pinned messages failed.
Returns
--------
List[:class:`~discord.Message`]
The messages that are currently pinned.
Yields
-------
:class:`~discord.Message`
The pinned message with :attr:`.Message.pinned_at` set.
"""
channel = await self._get_channel()
state = self._state
data = await state.http.pins_from(channel.id)
return [state.create_message(channel=channel, data=m) for m in data]
return _PinsIterator(self.__pins(limit=limit, before=before, oldest_first=oldest_first))
async def history(
self,
@ -1879,7 +2024,7 @@ class Messageable:
if limit is None:
raise ValueError('history does not support around with limit=None')
if limit > 101:
raise ValueError("history max limit 101 when specifying around parameter")
raise ValueError('history max limit 101 when specifying around parameter')
# Strange Discord quirk
limit = 100 if limit == 101 else limit

40
discord/activity.py

@ -28,7 +28,7 @@ import datetime
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union, overload
from .asset import Asset
from .enums import ActivityType, try_enum
from .enums import ActivityType, StatusDisplayType, try_enum
from .colour import Colour
from .partial_emoji import PartialEmoji
from .utils import _get_as_snowflake
@ -180,8 +180,10 @@ class Activity(BaseActivity):
- ``large_image``: A string representing the ID for the large image asset.
- ``large_text``: A string representing the text when hovering over the large image asset.
- ``large_url``: A string representing the URL of the large image asset.
- ``small_image``: A string representing the ID for the small image asset.
- ``small_text``: A string representing the text when hovering over the small image asset.
- ``small_url``: A string representing the URL of the small image asset.
party: :class:`dict`
A dictionary representing the activity party. It contains the following optional keys:
@ -195,6 +197,19 @@ class Activity(BaseActivity):
emoji: Optional[:class:`PartialEmoji`]
The emoji that belongs to this activity.
details_url: Optional[:class:`str`]
A URL that is linked to when clicking on the details text of the activity.
.. versionadded:: 2.6
state_url: Optional[:class:`str`]
A URL that is linked to when clicking on the state text of the activity.
.. versionadded:: 2.6
status_display_type: Optional[:class:`StatusDisplayType`]
Determines which field from the user's status text is displayed
in the members list.
.. versionadded:: 2.6
"""
__slots__ = (
@ -213,6 +228,9 @@ class Activity(BaseActivity):
'application_id',
'emoji',
'buttons',
'state_url',
'details_url',
'status_display_type',
)
def __init__(self, **kwargs: Any) -> None:
@ -239,6 +257,18 @@ class Activity(BaseActivity):
emoji = kwargs.pop('emoji', None)
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None
self.state_url: Optional[str] = kwargs.pop('state_url', None)
self.details_url: Optional[str] = kwargs.pop('details_url', None)
status_display_type = kwargs.pop('status_display_type', None)
self.status_display_type: Optional[StatusDisplayType] = (
status_display_type
if isinstance(status_display_type, StatusDisplayType)
else try_enum(StatusDisplayType, status_display_type)
if status_display_type is not None
else None
)
def __repr__(self) -> str:
attrs = (
('type', self.type),
@ -267,6 +297,8 @@ class Activity(BaseActivity):
ret['type'] = int(self.type)
if self.emoji:
ret['emoji'] = self.emoji.to_dict()
if self.status_display_type:
ret['status_display_type'] = int(self.status_display_type.value)
return ret
@property
@ -829,13 +861,11 @@ ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify]
@overload
def create_activity(data: ActivityPayload, state: ConnectionState) -> ActivityTypes:
...
def create_activity(data: ActivityPayload, state: ConnectionState) -> ActivityTypes: ...
@overload
def create_activity(data: None, state: ConnectionState) -> None:
...
def create_activity(data: None, state: ConnectionState) -> None: ...
def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> Optional[ActivityTypes]:

12
discord/app_commands/checks.py

@ -55,8 +55,9 @@ from ..utils import get as utils_get, MISSING, maybe_coroutine
T = TypeVar('T')
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from ..interactions import Interaction
from ..permissions import _PermissionsKwargs
CooldownFunction = Union[
Callable[[Interaction[Any]], Coroutine[Any, Any, T]],
@ -286,7 +287,7 @@ def has_any_role(*items: Union[int, str]) -> Callable[[T], T]:
return check(predicate)
def has_permissions(**perms: bool) -> Callable[[T], T]:
def has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
r"""A :func:`~discord.app_commands.check` that is added that checks if the member
has all of the permissions necessary.
@ -326,7 +327,7 @@ def has_permissions(**perms: bool) -> Callable[[T], T]:
invalid = perms.keys() - Permissions.VALID_FLAGS.keys()
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(interaction: Interaction) -> bool:
permissions = interaction.permissions
@ -341,7 +342,7 @@ def has_permissions(**perms: bool) -> Callable[[T], T]:
return check(predicate)
def bot_has_permissions(**perms: bool) -> Callable[[T], T]:
def bot_has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
"""Similar to :func:`has_permissions` except checks if the bot itself has
the permissions listed. This relies on :attr:`discord.Interaction.app_permissions`.
@ -353,7 +354,7 @@ def bot_has_permissions(**perms: bool) -> Callable[[T], T]:
invalid = set(perms) - set(Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(interaction: Interaction) -> bool:
permissions = interaction.app_permissions
@ -370,7 +371,6 @@ def bot_has_permissions(**perms: bool) -> Callable[[T], T]:
def _create_cooldown_decorator(
key: CooldownFunction[Hashable], factory: CooldownFunction[Optional[Cooldown]]
) -> Callable[[T], T]:
mapping: Dict[Any, Cooldown] = {}
async def get_bucket(

61
discord/app_commands/commands.py

@ -61,7 +61,7 @@ from ..permissions import Permissions
from ..utils import resolve_annotation, MISSING, is_inside_class, maybe_coroutine, async_all, _shorten, _to_kebab_case
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Concatenate
from typing_extensions import ParamSpec, Concatenate, Unpack
from ..interactions import Interaction
from ..abc import Snowflake
from .namespace import Namespace
@ -73,6 +73,7 @@ if TYPE_CHECKING:
# However, for type hinting purposes it's unfortunately necessary for one to
# reference the other to prevent type checking errors in callbacks
from discord.ext import commands
from discord.permissions import _PermissionsKwargs
ErrorFunc = Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]
@ -218,7 +219,7 @@ def validate_context_menu_name(name: str) -> str:
def validate_auto_complete_callback(
callback: AutocompleteCallback[GroupT, ChoiceT]
callback: AutocompleteCallback[GroupT, ChoiceT],
) -> AutocompleteCallback[GroupT, ChoiceT]:
# This function needs to ensure the following is true:
# If self.foo is passed then don't pass command.binding to the callback
@ -1490,9 +1491,9 @@ class Group:
__discord_app_commands_installation_types__: Optional[AppInstallationType] = MISSING
__discord_app_commands_default_permissions__: Optional[Permissions] = MISSING
__discord_app_commands_has_module__: bool = False
__discord_app_commands_error_handler__: Optional[
Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]
] = None
__discord_app_commands_error_handler__: Optional[Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]] = (
None
)
def __init_subclass__(
cls,
@ -2483,13 +2484,11 @@ def check(predicate: Check) -> Callable[[T], T]:
@overload
def guild_only(func: None = ...) -> Callable[[T], T]:
...
def guild_only(func: None = ...) -> Callable[[T], T]: ...
@overload
def guild_only(func: T) -> T:
...
def guild_only(func: T) -> T: ...
def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@ -2524,7 +2523,10 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.guild = True
# Ensure that only Guild context is allowed
allowed_contexts.guild = True # Enable guild context
allowed_contexts.private_channel = False # Disable private channel context
allowed_contexts.dm_channel = False # Disable DM context
return f
@ -2537,13 +2539,11 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@overload
def private_channel_only(func: None = ...) -> Callable[[T], T]:
...
def private_channel_only(func: None = ...) -> Callable[[T], T]: ...
@overload
def private_channel_only(func: T) -> T:
...
def private_channel_only(func: T) -> T: ...
def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@ -2578,7 +2578,10 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.private_channel = True
# Ensure that only Private Channel context is allowed
allowed_contexts.guild = False # Disable guild context
allowed_contexts.private_channel = True # Enable private channel context
allowed_contexts.dm_channel = False # Disable DM context
return f
@ -2591,13 +2594,11 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]
@overload
def dm_only(func: None = ...) -> Callable[[T], T]:
...
def dm_only(func: None = ...) -> Callable[[T], T]: ...
@overload
def dm_only(func: T) -> T:
...
def dm_only(func: T) -> T: ...
def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@ -2630,7 +2631,11 @@ def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.dm_channel = True
# Ensure that only DM context is allowed
allowed_contexts.guild = False # Disable guild context
allowed_contexts.private_channel = False # Disable private channel context
allowed_contexts.dm_channel = True # Enable DM context
return f
# Check if called with parentheses or not
@ -2686,13 +2691,11 @@ def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channe
@overload
def guild_install(func: None = ...) -> Callable[[T], T]:
...
def guild_install(func: None = ...) -> Callable[[T], T]: ...
@overload
def guild_install(func: T) -> T:
...
def guild_install(func: T) -> T: ...
def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@ -2724,6 +2727,7 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
allowed_installs.guild = True
allowed_installs.user = False
return f
@ -2736,13 +2740,11 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@overload
def user_install(func: None = ...) -> Callable[[T], T]:
...
def user_install(func: None = ...) -> Callable[[T], T]: ...
@overload
def user_install(func: T) -> T:
...
def user_install(func: T) -> T: ...
def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
@ -2774,6 +2776,7 @@ def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
allowed_installs.user = True
allowed_installs.guild = False
return f
@ -2828,7 +2831,7 @@ def allowed_installs(
return inner
def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: bool) -> Callable[[T], T]:
def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: Unpack[_PermissionsKwargs]) -> Callable[[T], T]:
r"""A decorator that sets the default permissions needed to execute this command.
When this decorator is used, by default users must have these permissions to execute the command.

104
discord/app_commands/models.py

@ -37,6 +37,7 @@ from ..enums import (
Locale,
try_enum,
)
import array
from ..mixins import Hashable
from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING
from ..object import Object
@ -84,7 +85,7 @@ if TYPE_CHECKING:
from ..abc import Snowflake
from ..state import ConnectionState
from ..guild import GuildChannel, Guild
from ..channel import TextChannel
from ..channel import TextChannel, ForumChannel, ForumTag
from ..threads import Thread
from ..user import User
@ -719,6 +720,14 @@ class AppCommandChannel(Hashable):
""":class:`str`: The string that allows you to mention the channel."""
return f'<#{self.id}>'
@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to the channel.
.. versionadded:: 2.6
"""
return f'https://discord.com/channels/{self.guild_id}/{self.id}'
@property
def created_at(self) -> datetime:
""":class:`datetime.datetime`: An aware timestamp of when this channel was created in UTC."""
@ -758,6 +767,34 @@ class AppCommandThread(Hashable):
The name of the thread.
parent_id: :class:`int`
The parent text channel ID this thread belongs to.
owner_id: :class:`int`
The user's ID that created this thread.
.. versionadded:: 2.6
last_message_id: Optional[:class:`int`]
The last message ID of the message sent to this thread. It may
*not* point to an existing or valid message.
.. versionadded:: 2.6
slowmode_delay: :class:`int`
The number of seconds a member must wait between sending messages
in this thread. A value of ``0`` denotes that it is disabled.
Bots and users with :attr:`~discord.Permissions.manage_channels` or
:attr:`~discord.Permissions.manage_messages` bypass slowmode.
.. versionadded:: 2.6
message_count: :class:`int`
An approximate number of messages in this thread.
.. versionadded:: 2.6
member_count: :class:`int`
An approximate number of members in this thread. This caps at 50.
.. versionadded:: 2.6
total_message_sent: :class:`int`
The total number of messages sent, including deleted messages.
.. versionadded:: 2.6
permissions: :class:`~discord.Permissions`
The resolved permissions of the user who invoked
the application command in that thread.
@ -792,6 +829,14 @@ class AppCommandThread(Hashable):
'archive_timestamp',
'locked',
'invitable',
'owner_id',
'message_count',
'member_count',
'slowmode_delay',
'last_message_id',
'total_message_sent',
'_applied_tags',
'_flags',
'_created_at',
'_state',
)
@ -810,6 +855,14 @@ class AppCommandThread(Hashable):
self.type: ChannelType = try_enum(ChannelType, data['type'])
self.name: str = data['name']
self.permissions: Permissions = Permissions(int(data['permissions']))
self.owner_id: int = int(data['owner_id'])
self.member_count: int = int(data['member_count'])
self.message_count: int = int(data['message_count'])
self.last_message_id: Optional[int] = _get_as_snowflake(data, 'last_message_id')
self.slowmode_delay: int = data.get('rate_limit_per_user', 0)
self.total_message_sent: int = data.get('total_message_sent', 0)
self._applied_tags: array.array[int] = array.array('Q', map(int, data.get('applied_tags', [])))
self._flags: int = data.get('flags', 0)
self._unroll_metadata(data['thread_metadata'])
def __str__(self) -> str:
@ -833,15 +886,58 @@ class AppCommandThread(Hashable):
self._created_at: Optional[datetime] = parse_time(data.get('create_timestamp'))
@property
def parent(self) -> Optional[TextChannel]:
"""Optional[:class:`~discord.TextChannel`]: The parent channel this thread belongs to."""
return self.guild.get_channel(self.parent_id) # type: ignore
def applied_tags(self) -> List[ForumTag]:
"""List[:class:`~discord.ForumTag`]: A list of tags applied to this thread.
.. versionadded:: 2.6
"""
tags = []
if self.parent is None or self.parent.type not in (ChannelType.forum, ChannelType.media):
return tags
parent = self.parent
for tag_id in self._applied_tags:
tag = parent.get_tag(tag_id) # type: ignore # parent here will be ForumChannel instance
if tag is not None:
tags.append(tag)
return tags
@property
def parent(self) -> Optional[Union[ForumChannel, TextChannel]]:
"""Optional[Union[:class:`~discord.ForumChannel`, :class:`~discord.TextChannel`]]: The parent channel
this thread belongs to."""
return self.guild and self.guild.get_channel(self.parent_id) # type: ignore
@property
def flags(self) -> ChannelFlags:
""":class:`~discord.ChannelFlags`: The flags associated with this thread.
.. versionadded:: 2.6
"""
return ChannelFlags._from_value(self._flags)
@property
def owner(self) -> Optional[Member]:
"""Optional[:class:`~discord.Member`]: The member this thread belongs to.
.. versionadded:: 2.6
"""
return self.guild and self.guild.get_member(self.owner_id)
@property
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the thread."""
return f'<#{self.id}>'
@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to the thread.
.. versionadded:: 2.6
"""
return f'https://discord.com/channels/{self.guild_id}/{self.id}'
@property
def created_at(self) -> Optional[datetime]:
"""An aware timestamp of when the thread was created in UTC.

6
discord/app_commands/namespace.py

@ -181,7 +181,7 @@ class Namespace:
guild_id = interaction.guild_id
guild = interaction.guild
type = AppCommandOptionType.user.value
for (user_id, user_data) in resolved.get('users', {}).items():
for user_id, user_data in resolved.get('users', {}).items():
try:
member_data = members[user_id]
except KeyError:
@ -203,7 +203,7 @@ class Namespace:
)
type = AppCommandOptionType.channel.value
for (channel_id, channel_data) in resolved.get('channels', {}).items():
for channel_id, channel_data in resolved.get('channels', {}).items():
key = ResolveKey(id=channel_id, type=type)
if channel_data['type'] in (10, 11, 12):
# The guild ID can't be none in this case
@ -220,7 +220,7 @@ class Namespace:
}
)
for (message_id, message_data) in resolved.get('messages', {}).items():
for message_id, message_data in resolved.get('messages', {}).items():
channel_id = int(message_data['channel_id'])
if guild is None:
channel = PartialMessageable(state=state, guild_id=guild_id, id=channel_id)

18
discord/app_commands/translator.py

@ -76,38 +76,32 @@ class TranslationContext(Generic[_L, _D]):
@overload
def __init__(
self, location: Literal[TranslationContextLocation.command_name], data: Union[Command[Any, ..., Any], ContextMenu]
) -> None:
...
) -> None: ...
@overload
def __init__(
self, location: Literal[TranslationContextLocation.command_description], data: Command[Any, ..., Any]
) -> None:
...
) -> None: ...
@overload
def __init__(
self,
location: Literal[TranslationContextLocation.group_name, TranslationContextLocation.group_description],
data: Group,
) -> None:
...
) -> None: ...
@overload
def __init__(
self,
location: Literal[TranslationContextLocation.parameter_name, TranslationContextLocation.parameter_description],
data: Parameter,
) -> None:
...
) -> None: ...
@overload
def __init__(self, location: Literal[TranslationContextLocation.choice_name], data: Choice[Any]) -> None:
...
def __init__(self, location: Literal[TranslationContextLocation.choice_name], data: Choice[Any]) -> None: ...
@overload
def __init__(self, location: Literal[TranslationContextLocation.other], data: Any) -> None:
...
def __init__(self, location: Literal[TranslationContextLocation.other], data: Any) -> None: ...
def __init__(self, location: _L, data: _D) -> None: # type: ignore # pyright doesn't like the overloads
self.location: _L = location

39
discord/app_commands/tree.py

@ -419,8 +419,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.message, AppCommandType.user],
) -> Optional[ContextMenu]:
...
) -> Optional[ContextMenu]: ...
@overload
def remove_command(
@ -430,8 +429,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.chat_input] = ...,
) -> Optional[Union[Command[Any, ..., Any], Group]]:
...
) -> Optional[Union[Command[Any, ..., Any], Group]]: ...
@overload
def remove_command(
@ -441,8 +439,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: AppCommandType,
) -> Optional[Union[Command[Any, ..., Any], ContextMenu, Group]]:
...
) -> Optional[Union[Command[Any, ..., Any], ContextMenu, Group]]: ...
def remove_command(
self,
@ -539,8 +536,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.message, AppCommandType.user],
) -> Optional[ContextMenu]:
...
) -> Optional[ContextMenu]: ...
@overload
def get_command(
@ -550,8 +546,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.chat_input] = ...,
) -> Optional[Union[Command[Any, ..., Any], Group]]:
...
) -> Optional[Union[Command[Any, ..., Any], Group]]: ...
@overload
def get_command(
@ -561,8 +556,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: AppCommandType,
) -> Optional[Union[Command[Any, ..., Any], ContextMenu, Group]]:
...
) -> Optional[Union[Command[Any, ..., Any], ContextMenu, Group]]: ...
def get_command(
self,
@ -613,8 +607,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.message, AppCommandType.user],
) -> List[ContextMenu]:
...
) -> List[ContextMenu]: ...
@overload
def get_commands(
@ -622,8 +615,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.chat_input],
) -> List[Union[Command[Any, ..., Any], Group]]:
...
) -> List[Union[Command[Any, ..., Any], Group]]: ...
@overload
def get_commands(
@ -631,8 +623,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: AppCommandType,
) -> Union[List[Union[Command[Any, ..., Any], Group]], List[ContextMenu]]:
...
) -> Union[List[Union[Command[Any, ..., Any], Group]], List[ContextMenu]]: ...
@overload
def get_commands(
@ -640,8 +631,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Optional[AppCommandType] = ...,
) -> List[Union[Command[Any, ..., Any], Group, ContextMenu]]:
...
) -> List[Union[Command[Any, ..., Any], Group, ContextMenu]]: ...
def get_commands(
self,
@ -693,8 +683,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.message, AppCommandType.user],
) -> Generator[ContextMenu, None, None]:
...
) -> Generator[ContextMenu, None, None]: ...
@overload
def walk_commands(
@ -702,8 +691,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: Literal[AppCommandType.chat_input] = ...,
) -> Generator[Union[Command[Any, ..., Any], Group], None, None]:
...
) -> Generator[Union[Command[Any, ..., Any], Group], None, None]: ...
@overload
def walk_commands(
@ -711,8 +699,7 @@ class CommandTree(Generic[ClientT]):
*,
guild: Optional[Snowflake] = ...,
type: AppCommandType,
) -> Union[Generator[Union[Command[Any, ..., Any], Group], None, None], Generator[ContextMenu, None, None]]:
...
) -> Union[Generator[Union[Command[Any, ..., Any], Group], None, None], Generator[ContextMenu, None, None]]: ...
def walk_commands(
self,

2
discord/appinfo.py

@ -406,7 +406,7 @@ class AppInfo:
if install_params_scopes is None:
install_params = None
else:
if "bot" not in install_params_scopes and install_params_permissions is not MISSING:
if 'bot' not in install_params_scopes and install_params_permissions is not MISSING:
raise ValueError("'bot' must be in install_params_scopes if install_params_permissions is set")
install_params['scopes'] = install_params_scopes

8
discord/asset.py

@ -50,8 +50,8 @@ if TYPE_CHECKING:
ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png']
ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif']
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
VALID_STATIC_FORMATS = frozenset({'jpeg', 'jpg', 'webp', 'png'})
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {'gif'}
MISSING = utils.MISSING
@ -241,7 +241,7 @@ class Asset(AssetMixin):
format = 'gif' if animated else 'png'
return cls(
state,
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024",
url=f'{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024',
key=avatar,
animated=animated,
)
@ -252,7 +252,7 @@ class Asset(AssetMixin):
format = 'gif' if animated else 'png'
return cls(
state,
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024",
url=f'{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024',
key=banner,
animated=animated,
)

37
discord/audit_logs.py

@ -44,6 +44,7 @@ from .sticker import GuildSticker
from .threads import Thread
from .integrations import PartialIntegration
from .channel import ForumChannel, StageChannel, ForumTag
from .onboarding import OnboardingPrompt, OnboardingPromptOption
__all__ = (
'AuditLogDiff',
@ -73,6 +74,7 @@ if TYPE_CHECKING:
from .types.snowflake import Snowflake
from .types.command import ApplicationCommandPermissions
from .types.automod import AutoModerationAction
from .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload
from .user import User
from .app_commands import AppCommand
from .webhook import Webhook
@ -246,6 +248,16 @@ def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji:
return PartialEmoji(name=data)
def _transform_onboarding_prompts(entry: AuditLogEntry, data: List[PromptPayload]) -> List[OnboardingPrompt]:
return [OnboardingPrompt.from_dict(data=prompt, state=entry._state, guild=entry.guild) for prompt in data]
def _transform_onboarding_prompt_options(
entry: AuditLogEntry, data: List[PromptOptionPayload]
) -> List[OnboardingPromptOption]:
return [OnboardingPromptOption.from_dict(data=option, state=entry._state, guild=entry.guild) for option in data]
E = TypeVar('E', bound=enums.Enum)
@ -268,13 +280,15 @@ def _flag_transformer(cls: Type[F]) -> Callable[[AuditLogEntry, Union[int, str]]
def _transform_type(
entry: AuditLogEntry, data: Union[int, str]
) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str]:
) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str, enums.OnboardingPromptType]:
if entry.action.name.startswith('sticker_'):
return enums.try_enum(enums.StickerType, data)
elif entry.action.name.startswith('integration_'):
return data # type: ignore # integration type is str
elif entry.action.name.startswith('webhook_'):
return enums.try_enum(enums.WebhookType, data)
elif entry.action.name.startswith('onboarding_prompt_'):
return enums.try_enum(enums.OnboardingPromptType, data)
else:
return enums.try_enum(enums.ChannelType, data)
@ -292,14 +306,12 @@ class AuditLogDiff:
if TYPE_CHECKING:
def __getattr__(self, item: str) -> Any:
...
def __getattr__(self, item: str) -> Any: ...
def __setattr__(self, key: str, value: Any) -> Any:
...
def __setattr__(self, key: str, value: Any) -> Any: ...
Transformer = Callable[["AuditLogEntry", Any], Any]
Transformer = Callable[['AuditLogEntry', Any], Any]
class AuditLogChanges:
@ -353,7 +365,11 @@ class AuditLogChanges:
'flags': (None, _transform_overloaded_flags),
'default_reaction_emoji': (None, _transform_default_reaction),
'emoji_name': ('emoji', _transform_default_emoji),
'user_id': ('user', _transform_member_id)
'user_id': ('user', _transform_member_id),
'options': (None, _transform_onboarding_prompt_options),
'prompts': (None, _transform_onboarding_prompts),
'default_channel_ids': ('default_channels', _transform_channels_or_threads),
'mode': (None, _enum_transformer(enums.OnboardingMode)),
}
# fmt: on
@ -761,7 +777,7 @@ class AuditLogEntry(Hashable):
self.extra = _AuditLogProxyAutoModAction(
automod_rule_name=extra['auto_moderation_rule_name'],
automod_rule_trigger_type=enums.try_enum(
enums.AutoModRuleTriggerType, extra['auto_moderation_rule_trigger_type']
enums.AutoModRuleTriggerType, int(extra['auto_moderation_rule_trigger_type'])
),
channel=channel,
)
@ -769,7 +785,7 @@ class AuditLogEntry(Hashable):
self.extra = _AuditLogProxyAutoModActionQuarantineUser(
automod_rule_name=extra['auto_moderation_rule_name'],
automod_rule_trigger_type=enums.try_enum(
enums.AutoModRuleTriggerType, extra['auto_moderation_rule_trigger_type']
enums.AutoModRuleTriggerType, int(extra['auto_moderation_rule_trigger_type'])
),
)
@ -977,3 +993,6 @@ class AuditLogEntry(Hashable):
from .webhook import Webhook
return self._webhooks.get(target_id) or Object(target_id, type=Webhook)
def _convert_target_onboarding_prompt(self, target_id: int) -> Object:
return Object(target_id, type=OnboardingPrompt)

26
discord/automod.py

@ -85,32 +85,27 @@ class AutoModRuleAction:
__slots__ = ('type', 'channel_id', 'duration', 'custom_message')
@overload
def __init__(self, *, channel_id: int = ...) -> None:
...
def __init__(self, *, channel_id: int = ...) -> None: ...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.send_alert_message], channel_id: int = ...) -> None:
...
def __init__(self, *, type: Literal[AutoModRuleActionType.send_alert_message], channel_id: int = ...) -> None: ...
@overload
def __init__(self, *, duration: datetime.timedelta = ...) -> None:
...
def __init__(self, *, duration: datetime.timedelta = ...) -> None: ...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.timeout], duration: datetime.timedelta = ...) -> None:
...
def __init__(self, *, type: Literal[AutoModRuleActionType.timeout], duration: datetime.timedelta = ...) -> None: ...
@overload
def __init__(self, *, custom_message: str = ...) -> None:
...
def __init__(self, *, custom_message: str = ...) -> None: ...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message]) -> None:
...
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message]) -> None: ...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message], custom_message: Optional[str] = ...) -> None:
...
def __init__(
self, *, type: Literal[AutoModRuleActionType.block_message], custom_message: Optional[str] = ...
) -> None: ...
@overload
def __init__(
@ -120,8 +115,7 @@ class AutoModRuleAction:
channel_id: Optional[int] = ...,
duration: Optional[datetime.timedelta] = ...,
custom_message: Optional[str] = ...,
) -> None:
...
) -> None: ...
def __init__(
self,

9
discord/backoff.py

@ -75,16 +75,13 @@ class ExponentialBackoff(Generic[T]):
self._randfunc: Callable[..., Union[int, float]] = rand.randrange if integral else rand.uniform
@overload
def delay(self: ExponentialBackoff[Literal[False]]) -> float:
...
def delay(self: ExponentialBackoff[Literal[False]]) -> float: ...
@overload
def delay(self: ExponentialBackoff[Literal[True]]) -> int:
...
def delay(self: ExponentialBackoff[Literal[True]]) -> int: ...
@overload
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]:
...
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]: ...
def delay(self) -> Union[int, float]:
"""Compute the next delay

145
discord/channel.py

@ -39,6 +39,7 @@ from typing import (
Sequence,
Tuple,
TypeVar,
TypedDict,
Union,
overload,
)
@ -85,7 +86,7 @@ __all__ = (
)
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from .types.threads import ThreadArchiveDuration
from .role import Role
@ -100,7 +101,7 @@ if TYPE_CHECKING:
from .file import File
from .user import ClientUser, User, BaseUser
from .guild import Guild, GuildChannel as GuildChannelType
from .ui.view import View
from .ui.view import BaseView, View, LayoutView
from .types.channel import (
TextChannel as TextChannelPayload,
NewsChannel as NewsChannelPayload,
@ -120,6 +121,44 @@ if TYPE_CHECKING:
OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object])
class _BaseCreateChannelOptions(TypedDict, total=False):
reason: Optional[str]
position: int
class _CreateTextChannelOptions(_BaseCreateChannelOptions, total=False):
topic: str
slowmode_delay: int
nsfw: bool
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
default_auto_archive_duration: int
default_thread_slowmode_delay: int
class _CreateVoiceChannelOptions(_BaseCreateChannelOptions, total=False):
bitrate: int
user_limit: int
rtc_region: Optional[str]
video_quality_mode: VideoQualityMode
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
class _CreateStageChannelOptions(_CreateVoiceChannelOptions, total=False):
bitrate: int
user_limit: int
rtc_region: Optional[str]
video_quality_mode: VideoQualityMode
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
class _CreateForumChannelOptions(_CreateTextChannelOptions, total=False):
topic: str
slowmode_delay: int
nsfw: bool
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite]
default_auto_archive_duration: int
default_thread_slowmode_delay: int
default_sort_order: ForumOrderType
default_reaction_emoji: EmojiInputType
default_layout: ForumLayoutType
available_tags: Sequence[ForumTag]
class ThreadWithMessage(NamedTuple):
thread: Thread
@ -168,7 +207,7 @@ class VoiceChannelSoundEffect(BaseSoundboardSound):
super().__init__(state=state, data=data)
def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} volume={self.volume}>"
return f'<{self.__class__.__name__} id={self.id} volume={self.volume}>'
@property
def created_at(self) -> Optional[datetime.datetime]:
@ -234,7 +273,7 @@ class VoiceChannelEffect:
('sound', self.sound),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f"<{self.__class__.__name__} {inner}>"
return f'<{self.__class__.__name__} {inner}>'
def is_sound(self) -> bool:
""":class:`bool`: Whether the effect is a sound or not."""
@ -418,12 +457,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
return self._state._get_message(self.last_message_id) if self.last_message_id else None
@overload
async def edit(self) -> Optional[TextChannel]:
...
async def edit(self) -> Optional[TextChannel]: ...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None: ...
@overload
async def edit(
@ -441,8 +478,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
default_thread_slowmode_delay: int = ...,
type: ChannelType = ...,
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
) -> TextChannel:
...
) -> TextChannel: ...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[TextChannel]:
"""|coro|
@ -1512,12 +1548,10 @@ class VoiceChannel(VocalGuildChannel):
return ChannelType.voice
@overload
async def edit(self) -> None:
...
async def edit(self) -> None: ...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None: ...
@overload
async def edit(
@ -1536,8 +1570,7 @@ class VoiceChannel(VocalGuildChannel):
slowmode_delay: int = ...,
status: Optional[str] = ...,
reason: Optional[str] = ...,
) -> VoiceChannel:
...
) -> VoiceChannel: ...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[VoiceChannel]:
"""|coro|
@ -1878,12 +1911,10 @@ class StageChannel(VocalGuildChannel):
return StageInstance(guild=self.guild, state=self._state, data=data)
@overload
async def edit(self) -> None:
...
async def edit(self) -> None: ...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None: ...
@overload
async def edit(
@ -1901,8 +1932,7 @@ class StageChannel(VocalGuildChannel):
video_quality_mode: VideoQualityMode = ...,
slowmode_delay: int = ...,
reason: Optional[str] = ...,
) -> StageChannel:
...
) -> StageChannel: ...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[StageChannel]:
"""|coro|
@ -2068,12 +2098,10 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
return await self._clone_impl({'nsfw': self.nsfw}, name=name, reason=reason)
@overload
async def edit(self) -> None:
...
async def edit(self) -> None: ...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None: ...
@overload
async def edit(
@ -2084,8 +2112,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
nsfw: bool = ...,
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
reason: Optional[str] = ...,
) -> CategoryChannel:
...
) -> CategoryChannel: ...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[CategoryChannel]:
"""|coro|
@ -2194,7 +2221,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
r.sort(key=lambda c: (c.position, c.id))
return r
async def create_text_channel(self, name: str, **options: Any) -> TextChannel:
async def create_text_channel(self, name: str, **options: Unpack[_CreateTextChannelOptions]) -> TextChannel:
"""|coro|
A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category.
@ -2206,7 +2233,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_text_channel(name, category=self, **options)
async def create_voice_channel(self, name: str, **options: Any) -> VoiceChannel:
async def create_voice_channel(self, name: str, **options: Unpack[_CreateVoiceChannelOptions]) -> VoiceChannel:
"""|coro|
A shortcut method to :meth:`Guild.create_voice_channel` to create a :class:`VoiceChannel` in the category.
@ -2218,7 +2245,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_voice_channel(name, category=self, **options)
async def create_stage_channel(self, name: str, **options: Any) -> StageChannel:
async def create_stage_channel(self, name: str, **options: Unpack[_CreateStageChannelOptions]) -> StageChannel:
"""|coro|
A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category.
@ -2232,7 +2259,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_stage_channel(name, category=self, **options)
async def create_forum(self, name: str, **options: Any) -> ForumChannel:
async def create_forum(self, name: str, **options: Unpack[_CreateForumChannelOptions]) -> ForumChannel:
"""|coro|
A shortcut method to :meth:`Guild.create_forum` to create a :class:`ForumChannel` in the category.
@ -2619,12 +2646,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
)
@overload
async def edit(self) -> None:
...
async def edit(self) -> None: ...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None: ...
@overload
async def edit(
@ -2647,8 +2672,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
default_layout: ForumLayoutType = ...,
default_sort_order: ForumOrderType = ...,
require_tag: bool = ...,
) -> ForumChannel:
...
) -> ForumChannel: ...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[ForumChannel]:
"""|coro|
@ -2841,6 +2865,45 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
return result
@overload
async def create_thread(
self,
*,
name: str,
auto_archive_duration: ThreadArchiveDuration = ...,
slowmode_delay: Optional[int] = ...,
file: File = ...,
files: Sequence[File] = ...,
allowed_mentions: AllowedMentions = ...,
mention_author: bool = ...,
applied_tags: Sequence[ForumTag] = ...,
view: LayoutView,
suppress_embeds: bool = ...,
reason: Optional[str] = ...,
) -> ThreadWithMessage: ...
@overload
async def create_thread(
self,
*,
name: str,
auto_archive_duration: ThreadArchiveDuration = ...,
slowmode_delay: Optional[int] = ...,
content: Optional[str] = ...,
tts: bool = ...,
embed: Embed = ...,
embeds: Sequence[Embed] = ...,
file: File = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
allowed_mentions: AllowedMentions = ...,
mention_author: bool = ...,
applied_tags: Sequence[ForumTag] = ...,
view: View = ...,
suppress_embeds: bool = ...,
reason: Optional[str] = ...,
) -> ThreadWithMessage: ...
async def create_thread(
self,
*,
@ -2857,7 +2920,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
allowed_mentions: AllowedMentions = MISSING,
mention_author: bool = MISSING,
applied_tags: Sequence[ForumTag] = MISSING,
view: View = MISSING,
view: BaseView = MISSING,
suppress_embeds: bool = False,
reason: Optional[str] = None,
) -> ThreadWithMessage:
@ -2907,7 +2970,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
applied_tags: List[:class:`discord.ForumTag`]
A list of tags to apply to the thread.
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
A Discord UI View to add to the message.
stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]]
A list of stickers to upload. Must be a maximum of 3.
@ -2983,7 +3046,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
data = await state.http.start_thread_in_forum(self.id, params=params, reason=reason)
thread = Thread(guild=self.guild, state=self._state, data=data)
message = Message(state=self._state, channel=thread, data=data['message'])
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
self._state.store_view(view, message.id)
return ThreadWithMessage(thread=thread, message=message)

243
discord/client.py

@ -42,6 +42,7 @@ from typing import (
Tuple,
Type,
TypeVar,
TypedDict,
Union,
overload,
)
@ -72,7 +73,7 @@ from .object import Object
from .backoff import ExponentialBackoff
from .webhook import Webhook
from .appinfo import AppInfo
from .ui.view import View
from .ui.view import BaseView
from .ui.dynamic import DynamicItem
from .stage_instance import StageInstance
from .threads import Thread
@ -82,7 +83,7 @@ from .soundboard import SoundboardDefaultSound, SoundboardSound
if TYPE_CHECKING:
from types import TracebackType
from typing_extensions import Self
from typing_extensions import Self, Unpack
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu
@ -120,6 +121,28 @@ if TYPE_CHECKING:
from .audit_logs import AuditLogEntry
from .poll import PollAnswer
from .subscription import Subscription
from .flags import MemberCacheFlags
class _ClientOptions(TypedDict, total=False):
max_messages: int
proxy: str
proxy_auth: aiohttp.BasicAuth
shard_id: int
shard_count: int
application_id: int
member_cache_flags: MemberCacheFlags
chunk_guilds_at_startup: bool
status: Status
activity: BaseActivity
allowed_mentions: AllowedMentions
heartbeat_timeout: float
guild_ready_timeout: float
assume_unsync_clock: bool
enable_debug_events: bool
enable_raw_presences: bool
http_trace: aiohttp.TraceConfig
max_ratelimit_timeout: float
connector: aiohttp.BaseConnector
# fmt: off
@ -272,7 +295,7 @@ class Client:
The websocket gateway the client is currently connected to. Could be ``None``.
"""
def __init__(self, *, intents: Intents, **options: Any) -> None:
def __init__(self, *, intents: Intents, **options: Unpack[_ClientOptions]) -> None:
self.loop: asyncio.AbstractEventLoop = _loop
# self.ws is set in the connect method
self.ws: DiscordWebSocket = None # type: ignore
@ -315,7 +338,7 @@ class Client:
if VoiceClient.warn_nacl:
VoiceClient.warn_nacl = False
_log.warning("PyNaCl is not installed, voice will NOT be supported")
_log.warning('PyNaCl is not installed, voice will NOT be supported')
async def __aenter__(self) -> Self:
await self._async_setup_hook()
@ -751,7 +774,7 @@ class Client:
raise
retry = backoff.delay()
_log.exception("Attempting a reconnect in %.2fs", retry)
_log.exception('Attempting a reconnect in %.2fs', retry)
await asyncio.sleep(retry)
# Always try to RESUME the connection
# If the connection is not RESUME-able then the gateway will invalidate the session.
@ -1215,8 +1238,7 @@ class Client:
*,
check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawAppCommandPermissionsUpdateEvent:
...
) -> RawAppCommandPermissionsUpdateEvent: ...
@overload
async def wait_for(
@ -1226,8 +1248,7 @@ class Client:
*,
check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]:
...
) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]: ...
# AutoMod
@ -1239,8 +1260,7 @@ class Client:
*,
check: Optional[Callable[[AutoModRule], bool]] = ...,
timeout: Optional[float] = ...,
) -> AutoModRule:
...
) -> AutoModRule: ...
@overload
async def wait_for(
@ -1250,8 +1270,7 @@ class Client:
*,
check: Optional[Callable[[AutoModAction], bool]] = ...,
timeout: Optional[float] = ...,
) -> AutoModAction:
...
) -> AutoModAction: ...
# Channels
@ -1263,8 +1282,7 @@ class Client:
*,
check: Optional[Callable[[GroupChannel, GroupChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[GroupChannel, GroupChannel]:
...
) -> Tuple[GroupChannel, GroupChannel]: ...
@overload
async def wait_for(
@ -1274,8 +1292,7 @@ class Client:
*,
check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[PrivateChannel, datetime.datetime]:
...
) -> Tuple[PrivateChannel, datetime.datetime]: ...
@overload
async def wait_for(
@ -1285,8 +1302,7 @@ class Client:
*,
check: Optional[Callable[[GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> GuildChannel:
...
) -> GuildChannel: ...
@overload
async def wait_for(
@ -1296,8 +1312,7 @@ class Client:
*,
check: Optional[Callable[[GuildChannel, GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[GuildChannel, GuildChannel]:
...
) -> Tuple[GuildChannel, GuildChannel]: ...
@overload
async def wait_for(
@ -1312,8 +1327,7 @@ class Client:
]
],
timeout: Optional[float] = ...,
) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]:
...
) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]: ...
@overload
async def wait_for(
@ -1323,8 +1337,7 @@ class Client:
*,
check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Messageable, Union[User, Member], datetime.datetime]:
...
) -> Tuple[Messageable, Union[User, Member], datetime.datetime]: ...
@overload
async def wait_for(
@ -1334,8 +1347,7 @@ class Client:
*,
check: Optional[Callable[[RawTypingEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawTypingEvent:
...
) -> RawTypingEvent: ...
# Debug & Gateway events
@ -1347,8 +1359,7 @@ class Client:
*,
check: Optional[Callable[[], bool]] = ...,
timeout: Optional[float] = ...,
) -> None:
...
) -> None: ...
@overload
async def wait_for(
@ -1358,8 +1369,7 @@ class Client:
*,
check: Optional[Callable[[int], bool]] = ...,
timeout: Optional[float] = ...,
) -> int:
...
) -> int: ...
@overload
async def wait_for(
@ -1369,8 +1379,7 @@ class Client:
*,
check: Optional[Callable[[str], bool]] = ...,
timeout: Optional[float] = ...,
) -> str:
...
) -> str: ...
@overload
async def wait_for(
@ -1380,8 +1389,7 @@ class Client:
*,
check: Optional[Callable[[Union[str, bytes]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Union[str, bytes]:
...
) -> Union[str, bytes]: ...
# Entitlements
@overload
@ -1392,8 +1400,7 @@ class Client:
*,
check: Optional[Callable[[Entitlement], bool]] = ...,
timeout: Optional[float] = ...,
) -> Entitlement:
...
) -> Entitlement: ...
# Guilds
@ -1410,8 +1417,7 @@ class Client:
*,
check: Optional[Callable[[Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Guild:
...
) -> Guild: ...
@overload
async def wait_for(
@ -1421,8 +1427,7 @@ class Client:
*,
check: Optional[Callable[[Guild, Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Guild]:
...
) -> Tuple[Guild, Guild]: ...
@overload
async def wait_for(
@ -1432,8 +1437,7 @@ class Client:
*,
check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]:
...
) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]: ...
@overload
async def wait_for(
@ -1443,8 +1447,7 @@ class Client:
*,
check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]:
...
) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]: ...
@overload
async def wait_for(
@ -1454,8 +1457,7 @@ class Client:
*,
check: Optional[Callable[[Invite], bool]] = ...,
timeout: Optional[float] = ...,
) -> Invite:
...
) -> Invite: ...
@overload
async def wait_for(
@ -1465,8 +1467,7 @@ class Client:
*,
check: Optional[Callable[[AuditLogEntry], bool]] = ...,
timeout: Optional[float] = ...,
) -> AuditLogEntry:
...
) -> AuditLogEntry: ...
# Integrations
@ -1478,8 +1479,7 @@ class Client:
*,
check: Optional[Callable[[Integration], bool]] = ...,
timeout: Optional[float] = ...,
) -> Integration:
...
) -> Integration: ...
@overload
async def wait_for(
@ -1489,8 +1489,7 @@ class Client:
*,
check: Optional[Callable[[Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Guild:
...
) -> Guild: ...
@overload
async def wait_for(
@ -1500,8 +1499,7 @@ class Client:
*,
check: Optional[Callable[[GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> GuildChannel:
...
) -> GuildChannel: ...
@overload
async def wait_for(
@ -1511,8 +1509,7 @@ class Client:
*,
check: Optional[Callable[[RawIntegrationDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawIntegrationDeleteEvent:
...
) -> RawIntegrationDeleteEvent: ...
# Interactions
@ -1524,8 +1521,7 @@ class Client:
*,
check: Optional[Callable[[Interaction[Self]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Interaction[Self]:
...
) -> Interaction[Self]: ...
# Members
@ -1537,8 +1533,7 @@ class Client:
*,
check: Optional[Callable[[Member], bool]] = ...,
timeout: Optional[float] = ...,
) -> Member:
...
) -> Member: ...
@overload
async def wait_for(
@ -1548,8 +1543,7 @@ class Client:
*,
check: Optional[Callable[[RawMemberRemoveEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMemberRemoveEvent:
...
) -> RawMemberRemoveEvent: ...
@overload
async def wait_for(
@ -1559,8 +1553,7 @@ class Client:
*,
check: Optional[Callable[[Member, Member], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Member, Member]:
...
) -> Tuple[Member, Member]: ...
@overload
async def wait_for(
@ -1570,8 +1563,7 @@ class Client:
*,
check: Optional[Callable[[User, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[User, User]:
...
) -> Tuple[User, User]: ...
@overload
async def wait_for(
@ -1581,8 +1573,7 @@ class Client:
*,
check: Optional[Callable[[Guild, Union[User, Member]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Union[User, Member]]:
...
) -> Tuple[Guild, Union[User, Member]]: ...
@overload
async def wait_for(
@ -1592,8 +1583,7 @@ class Client:
*,
check: Optional[Callable[[Guild, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, User]:
...
) -> Tuple[Guild, User]: ...
# Messages
@ -1605,8 +1595,7 @@ class Client:
*,
check: Optional[Callable[[Message], bool]] = ...,
timeout: Optional[float] = ...,
) -> Message:
...
) -> Message: ...
@overload
async def wait_for(
@ -1616,8 +1605,7 @@ class Client:
*,
check: Optional[Callable[[Message, Message], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Message, Message]:
...
) -> Tuple[Message, Message]: ...
@overload
async def wait_for(
@ -1627,8 +1615,7 @@ class Client:
*,
check: Optional[Callable[[List[Message]], bool]] = ...,
timeout: Optional[float] = ...,
) -> List[Message]:
...
) -> List[Message]: ...
@overload
async def wait_for(
@ -1638,8 +1625,7 @@ class Client:
*,
check: Optional[Callable[[RawMessageUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMessageUpdateEvent:
...
) -> RawMessageUpdateEvent: ...
@overload
async def wait_for(
@ -1649,8 +1635,7 @@ class Client:
*,
check: Optional[Callable[[RawMessageDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMessageDeleteEvent:
...
) -> RawMessageDeleteEvent: ...
@overload
async def wait_for(
@ -1660,8 +1645,7 @@ class Client:
*,
check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawBulkMessageDeleteEvent:
...
) -> RawBulkMessageDeleteEvent: ...
# Reactions
@ -1673,8 +1657,7 @@ class Client:
*,
check: Optional[Callable[[Reaction, Union[Member, User]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Reaction, Union[Member, User]]:
...
) -> Tuple[Reaction, Union[Member, User]]: ...
@overload
async def wait_for(
@ -1684,8 +1667,7 @@ class Client:
*,
check: Optional[Callable[[Message, List[Reaction]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Message, List[Reaction]]:
...
) -> Tuple[Message, List[Reaction]]: ...
@overload
async def wait_for(
@ -1695,8 +1677,7 @@ class Client:
*,
check: Optional[Callable[[Reaction], bool]] = ...,
timeout: Optional[float] = ...,
) -> Reaction:
...
) -> Reaction: ...
@overload
async def wait_for(
@ -1706,8 +1687,7 @@ class Client:
*,
check: Optional[Callable[[RawReactionActionEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionActionEvent:
...
) -> RawReactionActionEvent: ...
@overload
async def wait_for(
@ -1717,8 +1697,7 @@ class Client:
*,
check: Optional[Callable[[RawReactionClearEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionClearEvent:
...
) -> RawReactionClearEvent: ...
@overload
async def wait_for(
@ -1728,8 +1707,7 @@ class Client:
*,
check: Optional[Callable[[RawReactionClearEmojiEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionClearEmojiEvent:
...
) -> RawReactionClearEmojiEvent: ...
# Roles
@ -1741,8 +1719,7 @@ class Client:
*,
check: Optional[Callable[[Role], bool]] = ...,
timeout: Optional[float] = ...,
) -> Role:
...
) -> Role: ...
@overload
async def wait_for(
@ -1752,8 +1729,7 @@ class Client:
*,
check: Optional[Callable[[Role, Role], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Role, Role]:
...
) -> Tuple[Role, Role]: ...
# Scheduled Events
@ -1765,8 +1741,7 @@ class Client:
*,
check: Optional[Callable[[ScheduledEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def wait_for(
@ -1776,8 +1751,7 @@ class Client:
*,
check: Optional[Callable[[ScheduledEvent, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[ScheduledEvent, User]:
...
) -> Tuple[ScheduledEvent, User]: ...
# Stages
@ -1789,8 +1763,7 @@ class Client:
*,
check: Optional[Callable[[StageInstance], bool]] = ...,
timeout: Optional[float] = ...,
) -> StageInstance:
...
) -> StageInstance: ...
@overload
async def wait_for(
@ -1800,8 +1773,7 @@ class Client:
*,
check: Optional[Callable[[StageInstance, StageInstance], bool]] = ...,
timeout: Optional[float] = ...,
) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]:
...
) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]: ...
# Subscriptions
@overload
@ -1812,8 +1784,7 @@ class Client:
*,
check: Optional[Callable[[Subscription], bool]] = ...,
timeout: Optional[float] = ...,
) -> Subscription:
...
) -> Subscription: ...
# Threads
@overload
@ -1824,8 +1795,7 @@ class Client:
*,
check: Optional[Callable[[Thread], bool]] = ...,
timeout: Optional[float] = ...,
) -> Thread:
...
) -> Thread: ...
@overload
async def wait_for(
@ -1835,8 +1805,7 @@ class Client:
*,
check: Optional[Callable[[Thread, Thread], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Thread, Thread]:
...
) -> Tuple[Thread, Thread]: ...
@overload
async def wait_for(
@ -1846,8 +1815,7 @@ class Client:
*,
check: Optional[Callable[[RawThreadUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadUpdateEvent:
...
) -> RawThreadUpdateEvent: ...
@overload
async def wait_for(
@ -1857,8 +1825,7 @@ class Client:
*,
check: Optional[Callable[[RawThreadDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadDeleteEvent:
...
) -> RawThreadDeleteEvent: ...
@overload
async def wait_for(
@ -1868,8 +1835,7 @@ class Client:
*,
check: Optional[Callable[[ThreadMember], bool]] = ...,
timeout: Optional[float] = ...,
) -> ThreadMember:
...
) -> ThreadMember: ...
@overload
async def wait_for(
@ -1879,8 +1845,7 @@ class Client:
*,
check: Optional[Callable[[RawThreadMembersUpdate], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadMembersUpdate:
...
) -> RawThreadMembersUpdate: ...
# Voice
@ -1892,8 +1857,7 @@ class Client:
*,
check: Optional[Callable[[Member, VoiceState, VoiceState], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Member, VoiceState, VoiceState]:
...
) -> Tuple[Member, VoiceState, VoiceState]: ...
# Polls
@ -1905,8 +1869,7 @@ class Client:
*,
check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Union[User, Member], PollAnswer]:
...
) -> Tuple[Union[User, Member], PollAnswer]: ...
@overload
async def wait_for(
@ -1916,32 +1879,29 @@ class Client:
*,
check: Optional[Callable[[RawPollVoteActionEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawPollVoteActionEvent:
...
) -> RawPollVoteActionEvent: ...
# Commands
@overload
async def wait_for(
self: Union[Bot, AutoShardedBot],
event: Literal["command", "command_completion"],
event: Literal['command', 'command_completion'],
/,
*,
check: Optional[Callable[[Context[Any]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Context[Any]:
...
) -> Context[Any]: ...
@overload
async def wait_for(
self: Union[Bot, AutoShardedBot],
event: Literal["command_error"],
event: Literal['command_error'],
/,
*,
check: Optional[Callable[[Context[Any], CommandError], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Context[Any], CommandError]:
...
) -> Tuple[Context[Any], CommandError]: ...
@overload
async def wait_for(
@ -1951,8 +1911,7 @@ class Client:
*,
check: Optional[Callable[..., bool]] = ...,
timeout: Optional[float] = ...,
) -> Any:
...
) -> Any: ...
def wait_for(
self,
@ -2510,6 +2469,9 @@ class Client:
:attr:`.Invite.expires_at` field.
.. versionadded:: 2.0
.. deprecated:: 2.6
This parameter is deprecated and will be removed in a future version as it is no
longer needed to fill the :attr:`.Invite.expires_at` field.
scheduled_event_id: Optional[:class:`int`]
The ID of the scheduled event this invite is for.
@ -2545,7 +2507,6 @@ class Client:
data = await self.http.get_invite(
resolved.code,
with_counts=with_counts,
with_expiration=with_expiration,
guild_scheduled_event_id=scheduled_event_id,
)
return Invite.from_incomplete(state=self._connection, data=data)
@ -3154,7 +3115,7 @@ class Client:
self._connection.remove_dynamic_items(*items)
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None:
"""Registers a :class:`~discord.ui.View` for persistent listening.
This method should be used for when a view is comprised of components
@ -3164,7 +3125,7 @@ class Client:
Parameters
------------
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
The view to register for dispatching.
message_id: Optional[:class:`int`]
The message ID that the view is attached to. This is currently used to
@ -3180,7 +3141,7 @@ class Client:
and all their components have an explicitly provided custom_id.
"""
if not isinstance(view, View):
if not isinstance(view, BaseView):
raise TypeError(f'expected an instance of View not {view.__class__.__name__}')
if not view.is_persistent():
@ -3192,8 +3153,8 @@ class Client:
self._connection.store_view(view, message_id)
@property
def persistent_views(self) -> Sequence[View]:
"""Sequence[:class:`.View`]: A sequence of persistent views added to the client.
def persistent_views(self) -> Sequence[BaseView]:
"""Sequence[Union[:class:`.View`, :class:`.LayoutView`]]: A sequence of persistent views added to the client.
.. versionadded:: 2.0
"""

782
discord/components.py

@ -24,9 +24,31 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType
from .utils import get_slots, MISSING
from typing import (
ClassVar,
List,
Literal,
Optional,
TYPE_CHECKING,
Tuple,
Union,
)
from .asset import AssetMixin
from .enums import (
try_enum,
ComponentType,
ButtonStyle,
TextStyle,
ChannelType,
SelectDefaultValueType,
SeparatorSpacing,
MediaItemLoadingState,
)
from .flags import AttachmentFlags
from .colour import Colour
from .file import File
from .utils import get_slots, MISSING, _get_as_snowflake
from .partial_emoji import PartialEmoji, _EmojiTag
if TYPE_CHECKING:
@ -39,13 +61,36 @@ if TYPE_CHECKING:
SelectOption as SelectOptionPayload,
ActionRow as ActionRowPayload,
TextInput as TextInputPayload,
ActionRowChildComponent as ActionRowChildComponentPayload,
SelectDefaultValues as SelectDefaultValuesPayload,
SectionComponent as SectionComponentPayload,
TextComponent as TextComponentPayload,
MediaGalleryComponent as MediaGalleryComponentPayload,
FileComponent as FileComponentPayload,
SeparatorComponent as SeparatorComponentPayload,
MediaGalleryItem as MediaGalleryItemPayload,
ThumbnailComponent as ThumbnailComponentPayload,
ContainerComponent as ContainerComponentPayload,
UnfurledMediaItem as UnfurledMediaItemPayload,
LabelComponent as LabelComponentPayload,
)
from .emoji import Emoji
from .abc import Snowflake
from .state import ConnectionState
ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput']
SectionComponentType = Union['TextDisplay']
MessageComponentType = Union[
ActionRowChildComponentType,
SectionComponentType,
'ActionRow',
'SectionComponent',
'ThumbnailComponent',
'MediaGalleryComponent',
'FileComponent',
'SectionComponent',
'Component',
]
__all__ = (
@ -56,18 +101,36 @@ __all__ = (
'SelectOption',
'TextInput',
'SelectDefaultValue',
'SectionComponent',
'ThumbnailComponent',
'UnfurledMediaItem',
'MediaGalleryItem',
'MediaGalleryComponent',
'FileComponent',
'SectionComponent',
'Container',
'TextDisplay',
'SeparatorComponent',
'LabelComponent',
)
class Component:
"""Represents a Discord Bot UI Kit Component.
Currently, the only components supported by Discord are:
The components supported by Discord are:
- :class:`ActionRow`
- :class:`Button`
- :class:`SelectMenu`
- :class:`TextInput`
- :class:`SectionComponent`
- :class:`TextDisplay`
- :class:`ThumbnailComponent`
- :class:`MediaGalleryComponent`
- :class:`FileComponent`
- :class:`SeparatorComponent`
- :class:`Container`
This class is abstract and cannot be instantiated.
@ -116,20 +179,25 @@ class ActionRow(Component):
------------
children: List[Union[:class:`Button`, :class:`SelectMenu`, :class:`TextInput`]]
The children components that this holds, if any.
id: Optional[:class:`int`]
The ID of this component.
.. versionadded:: 2.6
"""
__slots__: Tuple[str, ...] = ('children',)
__slots__: Tuple[str, ...] = ('children', 'id')
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ActionRowPayload, /) -> None:
self.id: Optional[int] = data.get('id')
self.children: List[ActionRowChildComponentType] = []
for component_data in data.get('components', []):
component = _component_factory(component_data)
if component is not None:
self.children.append(component)
self.children.append(component) # type: ignore # should be the correct type here
@property
def type(self) -> Literal[ComponentType.action_row]:
@ -137,10 +205,13 @@ class ActionRow(Component):
return ComponentType.action_row
def to_dict(self) -> ActionRowPayload:
return {
payload: ActionRowPayload = {
'type': self.type.value,
'components': [child.to_dict() for child in self.children],
}
if self.id is not None:
payload['id'] = self.id
return payload
class Button(Component):
@ -174,6 +245,10 @@ class Button(Component):
The SKU ID this button sends you to, if available.
.. versionadded:: 2.4
id: Optional[:class:`int`]
The ID of this component.
.. versionadded:: 2.6
"""
__slots__: Tuple[str, ...] = (
@ -184,11 +259,13 @@ class Button(Component):
'label',
'emoji',
'sku_id',
'id',
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ButtonComponentPayload, /) -> None:
self.id: Optional[int] = data.get('id')
self.style: ButtonStyle = try_enum(ButtonStyle, data['style'])
self.custom_id: Optional[str] = data.get('custom_id')
self.url: Optional[str] = data.get('url')
@ -217,6 +294,9 @@ class Button(Component):
'disabled': self.disabled,
}
if self.id is not None:
payload['id'] = self.id
if self.sku_id:
payload['sku_id'] = str(self.sku_id)
@ -268,6 +348,14 @@ class SelectMenu(Component):
Whether the select is disabled or not.
channel_types: List[:class:`.ChannelType`]
A list of channel types that are allowed to be chosen in this select menu.
id: Optional[:class:`int`]
The ID of this component.
.. versionadded:: 2.6
required: :class:`bool`
Whether the select is required. Only applicable within modals.
.. versionadded:: 2.6
"""
__slots__: Tuple[str, ...] = (
@ -280,6 +368,8 @@ class SelectMenu(Component):
'disabled',
'channel_types',
'default_values',
'required',
'id',
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
@ -290,12 +380,14 @@ class SelectMenu(Component):
self.placeholder: Optional[str] = data.get('placeholder')
self.min_values: int = data.get('min_values', 1)
self.max_values: int = data.get('max_values', 1)
self.required: bool = data.get('required', False)
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])]
self.disabled: bool = data.get('disabled', False)
self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])]
self.default_values: List[SelectDefaultValue] = [
SelectDefaultValue.from_dict(d) for d in data.get('default_values', [])
]
self.id: Optional[int] = data.get('id')
def to_dict(self) -> SelectMenuPayload:
payload: SelectMenuPayload = {
@ -305,6 +397,8 @@ class SelectMenu(Component):
'max_values': self.max_values,
'disabled': self.disabled,
}
if self.id is not None:
payload['id'] = self.id
if self.placeholder:
payload['placeholder'] = self.placeholder
if self.options:
@ -312,7 +406,7 @@ class SelectMenu(Component):
if self.channel_types:
payload['channel_types'] = [t.value for t in self.channel_types]
if self.default_values:
payload["default_values"] = [v.to_dict() for v in self.default_values]
payload['default_values'] = [v.to_dict() for v in self.default_values]
return payload
@ -459,7 +553,7 @@ class TextInput(Component):
------------
custom_id: Optional[:class:`str`]
The ID of the text input that gets received during an interaction.
label: :class:`str`
label: Optional[:class:`str`]
The label to display above the text input.
style: :class:`TextStyle`
The style of the text input.
@ -473,6 +567,10 @@ class TextInput(Component):
The minimum length of the text input.
max_length: Optional[:class:`int`]
The maximum length of the text input.
id: Optional[:class:`int`]
The ID of this component.
.. versionadded:: 2.6
"""
__slots__: Tuple[str, ...] = (
@ -484,19 +582,21 @@ class TextInput(Component):
'required',
'min_length',
'max_length',
'id',
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: TextInputPayload, /) -> None:
self.style: TextStyle = try_enum(TextStyle, data['style'])
self.label: str = data['label']
self.label: Optional[str] = data.get('label')
self.custom_id: str = data['custom_id']
self.placeholder: Optional[str] = data.get('placeholder')
self.value: Optional[str] = data.get('value')
self.required: bool = data.get('required', True)
self.min_length: Optional[int] = data.get('min_length')
self.max_length: Optional[int] = data.get('max_length')
self.id: Optional[int] = data.get('id')
@property
def type(self) -> Literal[ComponentType.text_input]:
@ -512,6 +612,9 @@ class TextInput(Component):
'required': self.required,
}
if self.id is not None:
payload['id'] = self.id
if self.placeholder:
payload['placeholder'] = self.placeholder
@ -645,17 +748,642 @@ class SelectDefaultValue:
)
@overload
def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]:
...
class SectionComponent(Component):
"""Represents a section from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a section is :class:`discord.ui.Section`
not this one.
.. versionadded:: 2.6
Attributes
----------
children: List[:class:`TextDisplay`]
The components on this section.
accessory: :class:`Component`
The section accessory.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = (
'children',
'accessory',
'id',
)
__repr_info__ = __slots__
def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None:
self.children: List[SectionComponentType] = []
self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore
self.id: Optional[int] = data.get('id')
for component_data in data['components']:
component = _component_factory(component_data, state)
if component is not None:
self.children.append(component) # type: ignore # should be the correct type here
@property
def type(self) -> Literal[ComponentType.section]:
return ComponentType.section
def to_dict(self) -> SectionComponentPayload:
payload: SectionComponentPayload = {
'type': self.type.value,
'components': [c.to_dict() for c in self.children],
'accessory': self.accessory.to_dict(),
}
if self.id is not None:
payload['id'] = self.id
return payload
class ThumbnailComponent(Component):
"""Represents a Thumbnail from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a thumbnail is :class:`discord.ui.Thumbnail`
not this one.
.. versionadded:: 2.6
Attributes
----------
media: :class:`UnfurledMediaItem`
The media for this thumbnail.
description: Optional[:class:`str`]
The description shown within this thumbnail.
spoiler: :class:`bool`
Whether this thumbnail is flagged as a spoiler.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = (
'media',
'spoiler',
'description',
'id',
)
__repr_info__ = __slots__
def __init__(
self,
data: ThumbnailComponentPayload,
state: Optional[ConnectionState],
) -> None:
self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state)
self.description: Optional[str] = data.get('description')
self.spoiler: bool = data.get('spoiler', False)
self.id: Optional[int] = data.get('id')
@property
def type(self) -> Literal[ComponentType.thumbnail]:
return ComponentType.thumbnail
def to_dict(self) -> ThumbnailComponentPayload:
payload = {
'media': self.media.to_dict(),
'description': self.description,
'spoiler': self.spoiler,
'type': self.type.value,
}
if self.id is not None:
payload['id'] = self.id
return payload # type: ignore
@overload
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
...
class TextDisplay(Component):
"""Represents a text display from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a text display is
:class:`discord.ui.TextDisplay` not this one.
.. versionadded:: 2.6
Attributes
----------
content: :class:`str`
The content that this display shows.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = ('content', 'id')
__repr_info__ = __slots__
def __init__(self, data: TextComponentPayload) -> None:
self.content: str = data['content']
self.id: Optional[int] = data.get('id')
@property
def type(self) -> Literal[ComponentType.text_display]:
return ComponentType.text_display
def to_dict(self) -> TextComponentPayload:
payload: TextComponentPayload = {
'type': self.type.value,
'content': self.content,
}
if self.id is not None:
payload['id'] = self.id
return payload
class UnfurledMediaItem(AssetMixin):
"""Represents an unfurled media item.
.. versionadded:: 2.6
Parameters
----------
url: :class:`str`
The URL of this media item. This can be an arbitrary url or a reference to a local
file uploaded as an attachment within the message, which can be accessed with the
``attachment://<filename>`` format.
Attributes
----------
url: :class:`str`
The URL of this media item.
proxy_url: Optional[:class:`str`]
The proxy URL. This is a cached version of the :attr:`.url` in the
case of images. When the message is deleted, this URL might be valid for a few minutes
or not valid at all.
height: Optional[:class:`int`]
The media item's height, in pixels. Only applicable to images and videos.
width: Optional[:class:`int`]
The media item's width, in pixels. Only applicable to images and videos.
content_type: Optional[:class:`str`]
The media item's `media type <https://en.wikipedia.org/wiki/Media_type>`_
placeholder: Optional[:class:`str`]
The media item's placeholder.
loading_state: Optional[:class:`MediaItemLoadingState`]
The loading state of this media item.
attachment_id: Optional[:class:`int`]
The attachment id this media item points to, only available if the url points to a local file
uploaded within the component message.
"""
__slots__ = (
'url',
'proxy_url',
'height',
'width',
'content_type',
'_flags',
'placeholder',
'loading_state',
'attachment_id',
'_state',
)
def __init__(self, url: str) -> None:
self.url: str = url
self.proxy_url: Optional[str] = None
self.height: Optional[int] = None
self.width: Optional[int] = None
self.content_type: Optional[str] = None
self._flags: int = 0
self.placeholder: Optional[str] = None
self.loading_state: Optional[MediaItemLoadingState] = None
self.attachment_id: Optional[int] = None
self._state: Optional[ConnectionState] = None
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: This media item's flags."""
return AttachmentFlags._from_value(self._flags)
@classmethod
def _from_data(cls, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]):
self = cls(data['url'])
self._update(data, state)
return self
def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]) -> None:
self.proxy_url = data.get('proxy_url')
self.height = data.get('height')
self.width = data.get('width')
self.content_type = data.get('content_type')
self._flags = data.get('flags', 0)
self.placeholder = data.get('placeholder')
loading_state = data.get('loading_state')
if loading_state is not None:
self.loading_state = try_enum(MediaItemLoadingState, loading_state)
self.attachment_id = _get_as_snowflake(data, 'attachment_id')
self._state = state
def __repr__(self) -> str:
return f'<UnfurledMediaItem url={self.url}>'
def to_dict(self):
return {
'url': self.url,
}
class MediaGalleryItem:
"""Represents a :class:`MediaGalleryComponent` media item.
.. versionadded:: 2.6
Parameters
----------
media: Union[:class:`str`, :class:`discord.File`, :class:`UnfurledMediaItem`]
The media item data. This can be a string representing a local
file uploaded as an attachment in the message, which can be accessed
using the ``attachment://<filename>`` format, or an arbitrary url.
description: Optional[:class:`str`]
The description to show within this item. Up to 256 characters. Defaults
to ``None``.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler.
"""
__slots__ = (
'_media',
'description',
'spoiler',
'_state',
)
def __init__(
self,
media: Union[str, File, UnfurledMediaItem],
*,
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
) -> None:
self.media = media
if isinstance(media, File):
if description is MISSING:
description = media.description
if spoiler is MISSING:
spoiler = media.spoiler
self.description: Optional[str] = None if description is MISSING else description
self.spoiler: bool = bool(spoiler)
self._state: Optional[ConnectionState] = None
def __repr__(self) -> str:
return f'<MediaGalleryItem media={self.media!r}>'
@property
def media(self) -> UnfurledMediaItem:
""":class:`UnfurledMediaItem`: This item's media data."""
return self._media
@media.setter
def media(self, value: Union[str, File, UnfurledMediaItem]) -> None:
if isinstance(value, str):
self._media = UnfurledMediaItem(value)
elif isinstance(value, UnfurledMediaItem):
self._media = value
elif isinstance(value, File):
self._media = UnfurledMediaItem(value.uri)
else:
raise TypeError(f'Expected a str or UnfurledMediaItem, not {value.__class__.__name__}')
@classmethod
def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem:
media = data['media']
self = cls(
media=UnfurledMediaItem._from_data(media, state),
description=data.get('description'),
spoiler=data.get('spoiler', False),
)
self._state = state
return self
@classmethod
def _from_gallery(
cls,
items: List[MediaGalleryItemPayload],
state: Optional[ConnectionState],
) -> List[MediaGalleryItem]:
return [cls._from_data(item, state) for item in items]
def to_dict(self) -> MediaGalleryItemPayload:
payload: MediaGalleryItemPayload = {
'media': self.media.to_dict(), # type: ignore
'spoiler': self.spoiler,
}
if self.description:
payload['description'] = self.description
return payload
class MediaGalleryComponent(Component):
"""Represents a Media Gallery component from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type for creating a media gallery is
:class:`discord.ui.MediaGallery` not this one.
.. versionadded:: 2.6
Attributes
----------
items: List[:class:`MediaGalleryItem`]
The items this gallery has.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = ('items', 'id')
__repr_info__ = __slots__
def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None:
self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state)
self.id: Optional[int] = data.get('id')
@property
def type(self) -> Literal[ComponentType.media_gallery]:
return ComponentType.media_gallery
def to_dict(self) -> MediaGalleryComponentPayload:
payload: MediaGalleryComponentPayload = {
'type': self.type.value,
'items': [item.to_dict() for item in self.items],
}
if self.id is not None:
payload['id'] = self.id
return payload
class FileComponent(Component):
"""Represents a File component from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type for create a file component is
:class:`discord.ui.File` not this one.
.. versionadded:: 2.6
Attributes
----------
media: :class:`UnfurledMediaItem`
The unfurled attachment contents of the file.
spoiler: :class:`bool`
Whether this file is flagged as a spoiler.
id: Optional[:class:`int`]
The ID of this component.
name: Optional[:class:`str`]
The displayed file name, only available when received from the API.
size: Optional[:class:`int`]
The file size in MiB, only available when received from the API.
"""
__slots__ = (
'media',
'spoiler',
'id',
'name',
'size',
)
__repr_info__ = __slots__
def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None:
self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state)
self.spoiler: bool = data.get('spoiler', False)
self.id: Optional[int] = data.get('id')
self.name: Optional[str] = data.get('name')
self.size: Optional[int] = data.get('size')
@property
def type(self) -> Literal[ComponentType.file]:
return ComponentType.file
def to_dict(self) -> FileComponentPayload:
payload: FileComponentPayload = {
'type': self.type.value,
'file': self.media.to_dict(), # type: ignore
'spoiler': self.spoiler,
}
if self.id is not None:
payload['id'] = self.id
return payload
class SeparatorComponent(Component):
"""Represents a Separator from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type for creating a separator is
:class:`discord.ui.Separator` not this one.
.. versionadded:: 2.6
Attributes
----------
spacing: :class:`SeparatorSpacing`
The spacing size of the separator.
visible: :class:`bool`
Whether this separator is visible and shows a divider.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = (
'spacing',
'visible',
'id',
)
__repr_info__ = __slots__
def __init__(
self,
data: SeparatorComponentPayload,
) -> None:
self.spacing: SeparatorSpacing = try_enum(SeparatorSpacing, data.get('spacing', 1))
self.visible: bool = data.get('divider', True)
self.id: Optional[int] = data.get('id')
@property
def type(self) -> Literal[ComponentType.separator]:
return ComponentType.separator
def to_dict(self) -> SeparatorComponentPayload:
payload: SeparatorComponentPayload = {
'type': self.type.value,
'divider': self.visible,
'spacing': self.spacing.value,
}
if self.id is not None:
payload['id'] = self.id
return payload
class Container(Component):
"""Represents a Container from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type for creating a container is
:class:`discord.ui.Container` not this one.
.. versionadded:: 2.6
Attributes
----------
children: :class:`Component`
This container's children.
spoiler: :class:`bool`
Whether this container is flagged as a spoiler.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = (
'children',
'id',
'spoiler',
'_colour',
)
__repr_info__ = (
'children',
'id',
'spoiler',
'accent_colour',
)
def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None:
self.children: List[Component] = []
self.id: Optional[int] = data.get('id')
for child in data['components']:
comp = _component_factory(child, state)
if comp:
self.children.append(comp)
self.spoiler: bool = data.get('spoiler', False)
colour = data.get('accent_color')
self._colour: Optional[Colour] = None
if colour is not None:
self._colour = Colour(colour)
@property
def accent_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: The container's accent colour."""
return self._colour
accent_color = accent_colour
@property
def type(self) -> Literal[ComponentType.container]:
return ComponentType.container
def to_dict(self) -> ContainerComponentPayload:
payload: ContainerComponentPayload = {
'type': self.type.value,
'spoiler': self.spoiler,
'components': [c.to_dict() for c in self.children], # pyright: ignore[reportAssignmentType]
}
if self.id is not None:
payload['id'] = self.id
if self._colour:
payload['accent_color'] = self._colour.value
return payload
class LabelComponent(Component):
"""Represents a label component from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type for creating a label is
:class:`discord.ui.Label` not this one.
.. versionadded:: 2.6
Attributes
----------
label: :class:`str`
The label text to display.
description: Optional[:class:`str`]
The description text to display below the label, if any.
component: :class:`Component`
The component that this label is associated with.
id: Optional[:class:`int`]
The ID of this component.
"""
__slots__ = (
'label',
'description',
'commponent',
'id',
)
__repr_info__ = ('label', 'description', 'commponent', 'id,')
def __init__(self, data: LabelComponentPayload, state: Optional[ConnectionState]) -> None:
self.component: Component = _component_factory(data['component'], state) # type: ignore
self.label: str = data['label']
self.id: Optional[int] = data.get('id')
self.description: Optional[str] = data.get('description')
@property
def type(self) -> Literal[ComponentType.label]:
return ComponentType.label
def to_dict(self) -> LabelComponentPayload:
payload: LabelComponentPayload = {
'type': self.type.value,
'label': self.label,
'component': self.component.to_dict(), # type: ignore
}
if self.description:
payload['description'] = self.description
if self.id is not None:
payload['id'] = self.id
return payload
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]:
if data['type'] == 1:
return ActionRow(data)
elif data['type'] == 2:
@ -663,4 +1391,20 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti
elif data['type'] == 4:
return TextInput(data)
elif data['type'] in (3, 5, 6, 7, 8):
return SelectMenu(data)
return SelectMenu(data) # type: ignore
elif data['type'] == 9:
return SectionComponent(data, state)
elif data['type'] == 10:
return TextDisplay(data)
elif data['type'] == 11:
return ThumbnailComponent(data, state)
elif data['type'] == 12:
return MediaGalleryComponent(data, state)
elif data['type'] == 13:
return FileComponent(data, state)
elif data['type'] == 14:
return SeparatorComponent(data)
elif data['type'] == 17:
return Container(data, state)
elif data['type'] == 18:
return LabelComponent(data, state)

3
discord/embeds.py

@ -189,7 +189,6 @@ class Embed:
description: Optional[Any] = None,
timestamp: Optional[datetime.datetime] = None,
):
self.colour = colour if colour is not None else color
self.title: Optional[str] = title
self.type: EmbedType = type
@ -362,7 +361,7 @@ class Embed:
elif value is None:
self._timestamp = None
else:
raise TypeError(f"Expected datetime.datetime or None received {value.__class__.__name__} instead")
raise TypeError(f'Expected datetime.datetime or None received {value.__class__.__name__} instead')
@property
def footer(self) -> _EmbedFooterProxy:

70
discord/enums.py

@ -78,6 +78,11 @@ __all__ = (
'VoiceChannelEffectAnimationType',
'SubscriptionStatus',
'MessageReferenceType',
'StatusDisplayType',
'OnboardingPromptType',
'OnboardingMode',
'SeparatorSpacing',
'MediaItemLoadingState',
)
@ -171,7 +176,7 @@ class EnumMeta(type):
try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
raise ValueError(f"{value!r} is not a valid {cls.__name__}")
raise ValueError(f'{value!r} is not a valid {cls.__name__}')
def __getitem__(cls, key: str) -> Any:
return cls._enum_member_map_[key]
@ -401,6 +406,13 @@ class AuditLogAction(Enum):
automod_quarantine_user = 146
creator_monetization_request_created = 150
creator_monetization_terms_accepted = 151
onboarding_prompt_create = 163
onboarding_prompt_update = 164
onboarding_prompt_delete = 165
onboarding_create = 166
onboarding_update = 167
home_settings_create = 190
home_settings_update = 191
# fmt: on
@property
@ -467,6 +479,13 @@ class AuditLogAction(Enum):
AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create,
AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update,
AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete,
AuditLogAction.onboarding_prompt_create: AuditLogActionCategory.create,
AuditLogAction.onboarding_prompt_update: AuditLogActionCategory.update,
AuditLogAction.onboarding_prompt_delete: AuditLogActionCategory.delete,
AuditLogAction.onboarding_create: AuditLogActionCategory.create,
AuditLogAction.onboarding_update: AuditLogActionCategory.update,
AuditLogAction.home_settings_create: AuditLogActionCategory.create,
AuditLogAction.home_settings_update: AuditLogActionCategory.update,
}
# fmt: on
return lookup[self]
@ -512,6 +531,12 @@ class AuditLogAction(Enum):
return 'user'
elif v < 152:
return 'creator_monetization'
elif v < 166:
return 'onboarding_prompt'
elif v < 168:
return 'onboarding'
elif v < 192:
return 'home_settings'
class UserFlags(Enum):
@ -645,6 +670,14 @@ class ComponentType(Enum):
role_select = 6
mentionable_select = 7
channel_select = 8
section = 9
text_display = 10
thumbnail = 11
media_gallery = 12
file = 13
separator = 14
container = 17
label = 18
def __int__(self) -> int:
return self.value
@ -772,13 +805,6 @@ class Locale(Enum):
@property
def language_code(self) -> str:
""":class:`str`: Returns the locale's BCP 47 language code in the format of ``language-COUNTRY``.
This is derived from a predefined mapping based on Discord's supported locales.
If no mapping exists for the current locale, this returns the raw locale value as a fallback.
.. versionadded:: 2.6
"""
return _UNICODE_LANG_MAP.get(self.value, self.value)
@ -914,6 +940,34 @@ class SubscriptionStatus(Enum):
inactive = 2
class StatusDisplayType(Enum):
name = 0 # pyright: ignore[reportAssignmentType]
state = 1
details = 2
class OnboardingPromptType(Enum):
multiple_choice = 0
dropdown = 1
class OnboardingMode(Enum):
default = 0
advanced = 1
class SeparatorSpacing(Enum):
small = 1
large = 2
class MediaItemLoadingState(Enum):
unknown = 0
loading = 1
loaded = 2
not_found = 3
def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'

2
discord/errors.py

@ -261,7 +261,7 @@ class PrivilegedIntentsRequired(ClientException):
msg = (
'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the '
'developer portal. It is recommended to go to https://discord.com/developers/applications/ '
'and explicitly enable the privileged intents within your application\'s page. If this is not '
"and explicitly enable the privileged intents within your application's page. If this is not "
'possible, then consider disabling the privileged intents instead.'
)
super().__init__(msg % shard_id)

11
discord/ext/commands/_types.py

@ -22,7 +22,6 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import Any, Awaitable, Callable, Coroutine, TYPE_CHECKING, Protocol, TypeVar, Union, Tuple, Optional
@ -49,9 +48,9 @@ MaybeCoro = Union[T, Coro[T]]
MaybeAwaitable = Union[T, Awaitable[T]]
CogT = TypeVar('CogT', bound='Optional[Cog]')
UserCheck = Callable[["ContextT"], MaybeCoro[bool]]
Hook = Union[Callable[["CogT", "ContextT"], Coro[Any]], Callable[["ContextT"], Coro[Any]]]
Error = Union[Callable[["CogT", "ContextT", "CommandError"], Coro[Any]], Callable[["ContextT", "CommandError"], Coro[Any]]]
UserCheck = Callable[['ContextT'], MaybeCoro[bool]]
Hook = Union[Callable[['CogT', 'ContextT'], Coro[Any]], Callable[['ContextT'], Coro[Any]]]
Error = Union[Callable[['CogT', 'ContextT', 'CommandError'], Coro[Any]], Callable[['ContextT', 'CommandError'], Coro[Any]]]
ContextT = TypeVar('ContextT', bound='Context[Any]')
BotT = TypeVar('BotT', bound=_Bot, covariant=True)
@ -60,11 +59,9 @@ ContextT_co = TypeVar('ContextT_co', bound='Context[Any]', covariant=True)
class Check(Protocol[ContextT_co]): # type: ignore # TypeVar is expected to be invariant
predicate: Callable[[ContextT_co], Coroutine[Any, Any, bool]]
def __call__(self, coro_or_commands: T) -> T:
...
def __call__(self, coro_or_commands: T) -> T: ...
# This is merely a tag type to avoid circular import issues.

57
discord/ext/commands/bot.py

@ -64,7 +64,7 @@ from .cog import Cog
from .hybrid import hybrid_command, hybrid_group, HybridCommand, HybridGroup
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
import importlib.machinery
@ -80,12 +80,23 @@ if TYPE_CHECKING:
MaybeAwaitableFunc,
)
from .core import Command
from .hybrid import CommandCallback, ContextT, P
from .hybrid import CommandCallback, ContextT, P, _HybridCommandDecoratorKwargs, _HybridGroupDecoratorKwargs
from discord.client import _ClientOptions
from discord.shard import _AutoShardedClientOptions
_Prefix = Union[Iterable[str], str]
_PrefixCallable = MaybeAwaitableFunc[[BotT, Message], _Prefix]
PrefixType = Union[_Prefix, _PrefixCallable[BotT]]
class _BotOptions(_ClientOptions, total=False):
owner_id: int
owner_ids: Collection[int]
strip_after_prefix: bool
case_insensitive: bool
class _AutoShardedBotOptions(_AutoShardedClientOptions, _BotOptions): ...
__all__ = (
'when_mentioned',
'when_mentioned_or',
@ -169,7 +180,7 @@ class BotBase(GroupMixin[None]):
allowed_contexts: app_commands.AppCommandContext = MISSING,
allowed_installs: app_commands.AppInstallationType = MISSING,
intents: discord.Intents,
**options: Any,
**options: Unpack[_BotOptions],
) -> None:
super().__init__(intents=intents, **options)
self.command_prefix: PrefixType[BotT] = command_prefix # type: ignore
@ -281,7 +292,7 @@ class BotBase(GroupMixin[None]):
name: Union[str, app_commands.locale_str] = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridCommand[Any, P, T]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
the internal command list via :meth:`add_command`.
@ -293,8 +304,8 @@ class BotBase(GroupMixin[None]):
"""
def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self)
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for the user to set
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result
@ -305,7 +316,7 @@ class BotBase(GroupMixin[None]):
name: Union[str, app_commands.locale_str] = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridGroup[Any, P, T]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to
the internal command list via :meth:`add_command`.
@ -317,8 +328,8 @@ class BotBase(GroupMixin[None]):
"""
def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self)
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for the user to set
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result
@ -1221,8 +1232,8 @@ class BotBase(GroupMixin[None]):
raise
raise TypeError(
"command_prefix must be plain string, iterable of strings, or callable "
f"returning either of these, not {ret.__class__.__name__}"
'command_prefix must be plain string, iterable of strings, or callable '
f'returning either of these, not {ret.__class__.__name__}'
)
return ret
@ -1242,8 +1253,7 @@ class BotBase(GroupMixin[None]):
/,
*,
cls: Type[ContextT],
) -> ContextT:
...
) -> ContextT: ...
async def get_context(
self,
@ -1320,15 +1330,15 @@ class BotBase(GroupMixin[None]):
except TypeError:
if not isinstance(prefix, list):
raise TypeError(
"get_prefix must return either a string or a list of string, " f"not {prefix.__class__.__name__}"
f'get_prefix must return either a string or a list of string, not {prefix.__class__.__name__}'
)
# It's possible a bad command_prefix got us here.
for value in prefix:
if not isinstance(value, str):
raise TypeError(
"Iterable command_prefix or list returned from get_prefix must "
f"contain only strings, not {value.__class__.__name__}"
'Iterable command_prefix or list returned from get_prefix must '
f'contain only strings, not {value.__class__.__name__}'
)
# Getting here shouldn't happen
@ -1527,4 +1537,17 @@ class AutoShardedBot(BotBase, discord.AutoShardedClient):
.. versionadded:: 2.0
"""
pass
if TYPE_CHECKING:
def __init__(
self,
command_prefix: PrefixType[BotT],
*,
help_command: Optional[HelpCommand] = _default,
tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree,
description: Optional[str] = None,
allowed_contexts: app_commands.AppCommandContext = MISSING,
allowed_installs: app_commands.AppInstallationType = MISSING,
intents: discord.Intents,
**kwargs: Unpack[_AutoShardedBotOptions],
) -> None: ...

19
discord/ext/commands/cog.py

@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import inspect
@ -44,18 +45,30 @@ from typing import (
Tuple,
TypeVar,
Union,
TypedDict,
)
from ._types import _BaseCommand, BotT
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
from discord.abc import Snowflake
from discord._types import ClientT
from .bot import BotBase
from .context import Context
from .core import Command
from .core import Command, _CommandDecoratorKwargs
class _CogKwargs(TypedDict, total=False):
name: str
group_name: Union[str, app_commands.locale_str]
description: str
group_description: Union[str, app_commands.locale_str]
group_nsfw: bool
group_auto_locale_strings: bool
group_extras: Dict[Any, Any]
command_attrs: _CommandDecoratorKwargs
__all__ = (
'CogMeta',
@ -169,7 +182,7 @@ class CogMeta(type):
__cog_app_commands__: List[Union[app_commands.Group, app_commands.Command[Any, ..., Any]]]
__cog_listeners__: List[Tuple[str, str]]
def __new__(cls, *args: Any, **kwargs: Any) -> CogMeta:
def __new__(cls, *args: Any, **kwargs: Unpack[_CogKwargs]) -> CogMeta:
name, bases, attrs = args
if any(issubclass(base, app_commands.Group) for base in bases):
raise TypeError(

101
discord/ext/commands/context.py

@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import re
@ -48,7 +49,7 @@ if TYPE_CHECKING:
from discord.mentions import AllowedMentions
from discord.sticker import GuildSticker, StickerItem
from discord.message import MessageReference, PartialMessage
from discord.ui import View
from discord.ui.view import BaseView, View, LayoutView
from discord.types.interactions import ApplicationCommandInteractionData
from discord.poll import Poll
@ -70,7 +71,7 @@ MISSING: Any = discord.utils.MISSING
T = TypeVar('T')
CogT = TypeVar('CogT', bound="Cog")
CogT = TypeVar('CogT', bound='Cog')
if TYPE_CHECKING:
P = ParamSpec('P')
@ -424,8 +425,8 @@ class Context(discord.abc.Messageable, Generic[BotT]):
# consider this to be an *incredibly* strange use case. I'd rather go
# for this common use case rather than waste performance for the
# odd one.
pattern = re.compile(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
pattern = re.compile(r'<@!?%s>' % user.id)
return pattern.sub('@%s' % user.display_name.replace('\\', r'\\'), self.prefix)
@property
def cog(self) -> Optional[Cog]:
@ -628,6 +629,38 @@ class Context(discord.abc.Messageable, Generic[BotT]):
except CommandError as e:
await cmd.on_help_command_error(self, e)
@overload
async def reply(
self,
*,
file: File = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def reply(
self,
*,
files: Sequence[File] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def reply(
self,
@ -647,8 +680,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -669,8 +701,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -691,8 +722,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -713,8 +743,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
"""|coro|
@ -817,6 +846,38 @@ class Context(discord.abc.Messageable, Generic[BotT]):
if self.interaction:
await self.interaction.response.defer(ephemeral=ephemeral)
@overload
async def send(
self,
*,
file: File = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def send(
self,
*,
files: Sequence[File] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: LayoutView,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def send(
self,
@ -836,8 +897,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -858,8 +918,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -880,8 +939,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def send(
@ -902,8 +960,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
ephemeral: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
async def send(
self,
@ -920,7 +977,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
allowed_mentions: Optional[AllowedMentions] = None,
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
mention_author: Optional[bool] = None,
view: Optional[View] = None,
view: Optional[BaseView] = None,
suppress_embeds: bool = False,
ephemeral: bool = False,
silent: bool = False,
@ -986,7 +1043,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
This is ignored for interaction based contexts.
.. versionadded:: 1.6
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
A Discord UI View to add to the message.
.. versionadded:: 2.0

6
discord/ext/commands/converter.py

@ -1347,13 +1347,11 @@ async def _actual_conversion(ctx: Context[BotT], converter: Any, argument: str,
@overload
async def run_converters(
ctx: Context[BotT], converter: Union[Type[Converter[T]], Converter[T]], argument: str, param: Parameter
) -> T:
...
) -> T: ...
@overload
async def run_converters(ctx: Context[BotT], converter: Any, argument: str, param: Parameter) -> Any:
...
async def run_converters(ctx: Context[BotT], converter: Any, argument: str, param: Parameter) -> Any: ...
async def run_converters(ctx: Context[BotT], converter: Any, argument: str, param: Parameter) -> Any:

4
discord/ext/commands/cooldowns.py

@ -242,10 +242,10 @@ class MaxConcurrency:
self.wait: bool = wait
if number <= 0:
raise ValueError('max_concurrency \'number\' cannot be less than 1')
raise ValueError("max_concurrency 'number' cannot be less than 1")
if not isinstance(per, BucketType):
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
raise TypeError(f"max_concurrency 'per' must be of type BucketType not {type(per)!r}")
def copy(self) -> Self:
return self.__class__(self.number, per=self.per, wait=self.wait)

138
discord/ext/commands/core.py

@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import asyncio
@ -43,6 +44,7 @@ from typing import (
TypeVar,
Union,
overload,
TypedDict,
)
import re
@ -58,10 +60,39 @@ from .parameters import Parameter, Signature
from discord.app_commands.commands import NUMPY_DOCSTRING_ARG_REGEX
if TYPE_CHECKING:
from typing_extensions import Concatenate, ParamSpec, Self
from typing_extensions import Concatenate, ParamSpec, Self, Unpack
from ._types import BotT, Check, ContextT, Coro, CoroFunc, Error, Hook, UserCheck
from discord.permissions import _PermissionsKwargs
class _CommandDecoratorKwargs(TypedDict, total=False):
enabled: bool
help: str
brief: str
usage: str
rest_is_raw: bool
aliases: List[str]
description: str
hidden: bool
checks: List[UserCheck[Context[Any]]]
cooldown: CooldownMapping[Context[Any]]
max_concurrency: MaxConcurrency
require_var_positional: bool
cooldown_after_parsing: bool
ignore_extra: bool
extras: Dict[Any, Any]
class _CommandKwargs(_CommandDecoratorKwargs, total=False):
name: str
class _GroupDecoratorKwargs(_CommandDecoratorKwargs, total=False):
invoke_without_command: bool
case_insensitive: bool
class _GroupKwargs(_GroupDecoratorKwargs, total=False):
name: str
__all__ = (
'Command',
@ -368,6 +399,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
.. versionadded:: 2.0
"""
__original_kwargs__: Dict[str, Any]
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
@ -393,7 +425,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
Callable[Concatenate[Context[Any], P], Coro[T]],
],
/,
**kwargs: Any,
**kwargs: Unpack[_CommandKwargs],
) -> None:
if not asyncio.iscoroutinefunction(func):
raise TypeError('Callback must be a coroutine.')
@ -421,7 +453,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
self.extras: Dict[Any, Any] = kwargs.get('extras', {})
if not isinstance(self.aliases, (list, tuple)):
raise TypeError("Aliases of a command must be a list or a tuple of strings.")
raise TypeError('Aliases of a command must be a list or a tuple of strings.')
self.description: str = inspect.cleandoc(kwargs.get('description', ''))
self.hidden: bool = kwargs.get('hidden', False)
@ -444,7 +476,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
elif isinstance(cooldown, CooldownMapping):
buckets: CooldownMapping[Context[Any]] = cooldown
else:
raise TypeError("Cooldown must be an instance of CooldownMapping or None.")
raise TypeError('Cooldown must be an instance of CooldownMapping or None.')
self._buckets: CooldownMapping[Context[Any]] = buckets
try:
@ -490,7 +522,10 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
@property
def callback(
self,
) -> Union[Callable[Concatenate[CogT, Context[Any], P], Coro[T]], Callable[Concatenate[Context[Any], P], Coro[T]],]:
) -> Union[
Callable[Concatenate[CogT, Context[Any], P], Coro[T]],
Callable[Concatenate[Context[Any], P], Coro[T]],
]:
return self._callback
@callback.setter
@ -556,7 +591,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
except ValueError:
pass
def update(self, **kwargs: Any) -> None:
def update(self, **kwargs: Unpack[_CommandKwargs]) -> None:
"""Updates :class:`Command` instance with updated attribute.
This works similarly to the :func:`~discord.ext.commands.command` decorator in terms
@ -1468,7 +1503,7 @@ class GroupMixin(Generic[CogT]):
self: GroupMixin[CogT],
name: str = ...,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_CommandDecoratorKwargs],
) -> Callable[
[
Union[
@ -1477,8 +1512,7 @@ class GroupMixin(Generic[CogT]):
]
],
Command[CogT, P, T],
]:
...
]: ...
@overload
def command(
@ -1486,7 +1520,7 @@ class GroupMixin(Generic[CogT]):
name: str = ...,
cls: Type[CommandT] = ..., # type: ignore # previous overload handles case where cls is not set
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_CommandDecoratorKwargs],
) -> Callable[
[
Union[
@ -1495,15 +1529,14 @@ class GroupMixin(Generic[CogT]):
]
],
CommandT,
]:
...
]: ...
def command(
self,
name: str = MISSING,
cls: Type[Command[Any, ..., Any]] = MISSING,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_CommandDecoratorKwargs],
) -> Any:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.command` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
@ -1515,8 +1548,7 @@ class GroupMixin(Generic[CogT]):
"""
def decorator(func):
kwargs.setdefault('parent', self)
kwargs.setdefault('parent', self) # type: ignore # the parent kwarg is not for users to set.
result = command(name=name, cls=cls, *args, **kwargs)(func)
self.add_command(result)
return result
@ -1528,7 +1560,7 @@ class GroupMixin(Generic[CogT]):
self: GroupMixin[CogT],
name: str = ...,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_GroupDecoratorKwargs],
) -> Callable[
[
Union[
@ -1537,8 +1569,7 @@ class GroupMixin(Generic[CogT]):
]
],
Group[CogT, P, T],
]:
...
]: ...
@overload
def group(
@ -1546,7 +1577,7 @@ class GroupMixin(Generic[CogT]):
name: str = ...,
cls: Type[GroupT] = ..., # type: ignore # previous overload handles case where cls is not set
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_GroupDecoratorKwargs],
) -> Callable[
[
Union[
@ -1555,15 +1586,14 @@ class GroupMixin(Generic[CogT]):
]
],
GroupT,
]:
...
]: ...
def group(
self,
name: str = MISSING,
cls: Type[Group[Any, ..., Any]] = MISSING,
*args: Any,
**kwargs: Any,
**kwargs: Unpack[_GroupDecoratorKwargs],
) -> Any:
"""A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
@ -1575,7 +1605,7 @@ class GroupMixin(Generic[CogT]):
"""
def decorator(func):
kwargs.setdefault('parent', self)
kwargs.setdefault('parent', self) # type: ignore # the parent kwarg is not for users to set.
result = group(name=name, cls=cls, *args, **kwargs)(func)
self.add_command(result)
return result
@ -1606,7 +1636,7 @@ class Group(GroupMixin[CogT], Command[CogT, P, T]):
Defaults to ``False``.
"""
def __init__(self, *args: Any, **attrs: Any) -> None:
def __init__(self, *args: Any, **attrs: Unpack[_GroupKwargs]) -> None:
self.invoke_without_command: bool = attrs.pop('invoke_without_command', False)
super().__init__(*args, **attrs)
@ -1702,42 +1732,35 @@ if TYPE_CHECKING:
class _CommandDecorator:
@overload
def __call__(self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]], /) -> Command[CogT, P, T]:
...
def __call__(self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]], /) -> Command[CogT, P, T]: ...
@overload
def __call__(self, func: Callable[Concatenate[ContextT, P], Coro[T]], /) -> Command[None, P, T]:
...
def __call__(self, func: Callable[Concatenate[ContextT, P], Coro[T]], /) -> Command[None, P, T]: ...
def __call__(self, func: Callable[..., Coro[T]], /) -> Any:
...
def __call__(self, func: Callable[..., Coro[T]], /) -> Any: ...
class _GroupDecorator:
@overload
def __call__(self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]], /) -> Group[CogT, P, T]:
...
def __call__(self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]], /) -> Group[CogT, P, T]: ...
@overload
def __call__(self, func: Callable[Concatenate[ContextT, P], Coro[T]], /) -> Group[None, P, T]:
...
def __call__(self, func: Callable[Concatenate[ContextT, P], Coro[T]], /) -> Group[None, P, T]: ...
def __call__(self, func: Callable[..., Coro[T]], /) -> Any:
...
def __call__(self, func: Callable[..., Coro[T]], /) -> Any: ...
@overload
def command(
name: str = ...,
**attrs: Any,
) -> _CommandDecorator:
...
**attrs: Unpack[_CommandDecoratorKwargs],
) -> _CommandDecorator: ...
@overload
def command(
name: str = ...,
cls: Type[CommandT] = ..., # type: ignore # previous overload handles case where cls is not set
**attrs: Any,
**attrs: Unpack[_CommandDecoratorKwargs],
) -> Callable[
[
Union[
@ -1746,14 +1769,13 @@ def command(
]
],
CommandT,
]:
...
]: ...
def command(
name: str = MISSING,
cls: Type[Command[Any, ..., Any]] = MISSING,
**attrs: Any,
**attrs: Unpack[_CommandDecoratorKwargs],
) -> Any:
"""A decorator that transforms a function into a :class:`.Command`
or if called with :func:`.group`, :class:`.Group`.
@ -1798,16 +1820,15 @@ def command(
@overload
def group(
name: str = ...,
**attrs: Any,
) -> _GroupDecorator:
...
**attrs: Unpack[_GroupDecoratorKwargs],
) -> _GroupDecorator: ...
@overload
def group(
name: str = ...,
cls: Type[GroupT] = ..., # type: ignore # previous overload handles case where cls is not set
**attrs: Any,
**attrs: Unpack[_GroupDecoratorKwargs],
) -> Callable[
[
Union[
@ -1816,14 +1837,13 @@ def group(
]
],
GroupT,
]:
...
]: ...
def group(
name: str = MISSING,
cls: Type[Group[Any, ..., Any]] = MISSING,
**attrs: Any,
**attrs: Unpack[_GroupDecoratorKwargs],
) -> Any:
"""A decorator that transforms a function into a :class:`.Group`.
@ -2165,7 +2185,7 @@ def bot_has_any_role(*items: int) -> Callable[[T], T]:
return check(predicate)
def has_permissions(**perms: bool) -> Check[Any]:
def has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]:
"""A :func:`.check` that is added that checks if the member has all of
the permissions necessary.
@ -2197,7 +2217,7 @@ def has_permissions(**perms: bool) -> Check[Any]:
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(ctx: Context[BotT]) -> bool:
permissions = ctx.permissions
@ -2212,7 +2232,7 @@ def has_permissions(**perms: bool) -> Check[Any]:
return check(predicate)
def bot_has_permissions(**perms: bool) -> Check[Any]:
def bot_has_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]:
"""Similar to :func:`.has_permissions` except checks if the bot itself has
the permissions listed.
@ -2222,7 +2242,7 @@ def bot_has_permissions(**perms: bool) -> Check[Any]:
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(ctx: Context[BotT]) -> bool:
permissions = ctx.bot_permissions
@ -2237,7 +2257,7 @@ def bot_has_permissions(**perms: bool) -> Check[Any]:
return check(predicate)
def has_guild_permissions(**perms: bool) -> Check[Any]:
def has_guild_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]:
"""Similar to :func:`.has_permissions`, but operates on guild wide
permissions instead of the current channel permissions.
@ -2249,7 +2269,7 @@ def has_guild_permissions(**perms: bool) -> Check[Any]:
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(ctx: Context[BotT]) -> bool:
if not ctx.guild:
@ -2266,7 +2286,7 @@ def has_guild_permissions(**perms: bool) -> Check[Any]:
return check(predicate)
def bot_has_guild_permissions(**perms: bool) -> Check[Any]:
def bot_has_guild_permissions(**perms: Unpack[_PermissionsKwargs]) -> Check[Any]:
"""Similar to :func:`.has_guild_permissions`, but checks the bot
members guild permissions.
@ -2275,7 +2295,7 @@ def bot_has_guild_permissions(**perms: bool) -> Check[Any]:
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
raise TypeError(f'Invalid permission(s): {", ".join(invalid)}')
def predicate(ctx: Context[BotT]) -> bool:
if not ctx.guild:
@ -2515,7 +2535,7 @@ def dynamic_cooldown(
The type of cooldown to have.
"""
if not callable(cooldown):
raise TypeError("A callable must be provided")
raise TypeError('A callable must be provided')
if type is BucketType.default:
raise ValueError('BucketType.default cannot be used in dynamic cooldowns')

2
discord/ext/commands/errors.py

@ -925,7 +925,7 @@ class BadLiteralArgument(UserInputError):
.. versionadded:: 2.3
"""
def __init__(self, param: Parameter, literals: Tuple[Any, ...], errors: List[CommandError], argument: str = "") -> None:
def __init__(self, param: Parameter, literals: Tuple[Any, ...], errors: List[CommandError], argument: str = '') -> None:
self.param: Parameter = param
self.literals: Tuple[Any, ...] = literals
self.errors: List[CommandError] = errors

2
discord/ext/commands/flags.py

@ -197,7 +197,7 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s
if flag.positional:
if positional is not None:
raise TypeError(f"{flag.name!r} positional flag conflicts with {positional.name!r} flag.")
raise TypeError(f'{flag.name!r} positional flag conflicts with {positional.name!r} flag.')
positional = flag
annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache)

53
discord/ext/commands/help.py

@ -42,6 +42,7 @@ from typing import (
Iterable,
Sequence,
Mapping,
TypedDict,
)
import discord.utils
@ -50,7 +51,7 @@ from .core import Group, Command, get_signature_parameters
from .errors import CommandError
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
import discord.abc
@ -58,6 +59,7 @@ if TYPE_CHECKING:
from .context import Context
from .cog import Cog
from .parameters import Parameter
from .core import _CommandKwargs
from ._types import (
UserCheck,
@ -65,6 +67,30 @@ if TYPE_CHECKING:
_Bot,
)
class _HelpCommandOptions(TypedDict, total=False):
show_hidden: bool
verify_checks: bool
command_attrs: _CommandKwargs
class _BaseHelpCommandOptions(_HelpCommandOptions, total=False):
sort_commands: bool
dm_help: bool
dm_help_threshold: int
no_category: str
paginator: Paginator
commands_heading: str
class _DefaultHelpCommandOptions(_BaseHelpCommandOptions, total=False):
width: int
indent: int
arguments_heading: str
default_argument_description: str
show_parameter_descriptions: bool
class _MinimalHelpCommandOptions(_BaseHelpCommandOptions, total=False):
aliases_heading: str
__all__ = (
'Paginator',
'HelpCommand',
@ -224,7 +250,7 @@ def _not_overridden(f: FuncT) -> FuncT:
class _HelpCommandImpl(Command):
def __init__(self, inject: HelpCommand, *args: Any, **kwargs: Any) -> None:
def __init__(self, inject: HelpCommand, *args: Any, **kwargs: Unpack[_CommandKwargs]) -> None:
super().__init__(inject.command_callback, *args, **kwargs)
self._original: HelpCommand = inject
self._injected: HelpCommand = inject
@ -299,7 +325,7 @@ class _HelpCommandImpl(Command):
def update(self, **kwargs: Any) -> None:
cog = self.cog
self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs))
self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs)) # type: ignore
self.cog = cog
@ -366,10 +392,9 @@ class HelpCommand:
self.__original_args__ = deepcopy(args)
return self
def __init__(self, **options: Any) -> None:
def __init__(self, **options: Unpack[_HelpCommandOptions]) -> None:
self.show_hidden: bool = options.pop('show_hidden', False)
self.verify_checks: bool = options.pop('verify_checks', True)
self.command_attrs: Dict[str, Any]
self.command_attrs = attrs = options.pop('command_attrs', {})
attrs.setdefault('name', 'help')
attrs.setdefault('help', 'Shows this message')
@ -1041,21 +1066,23 @@ class DefaultHelpCommand(HelpCommand):
The paginator used to paginate the help command output.
"""
def __init__(self, **options: Any) -> None:
def __init__(self, **options: Unpack[_DefaultHelpCommandOptions]) -> None:
self.width: int = options.pop('width', 80)
self.indent: int = options.pop('indent', 2)
self.sort_commands: bool = options.pop('sort_commands', True)
self.dm_help: bool = options.pop('dm_help', False)
self.dm_help_threshold: int = options.pop('dm_help_threshold', 1000)
self.arguments_heading: str = options.pop('arguments_heading', "Arguments:")
self.arguments_heading: str = options.pop('arguments_heading', 'Arguments:')
self.commands_heading: str = options.pop('commands_heading', 'Commands:')
self.default_argument_description: str = options.pop('default_argument_description', 'No description given')
self.no_category: str = options.pop('no_category', 'No Category')
self.paginator: Paginator = options.pop('paginator', None)
self.show_parameter_descriptions: bool = options.pop('show_parameter_descriptions', True)
if self.paginator is None:
paginator = options.pop('paginator', None)
if paginator is None:
self.paginator: Paginator = Paginator()
else:
self.paginator: Paginator = paginator
super().__init__(**options)
@ -1334,17 +1361,19 @@ class MinimalHelpCommand(HelpCommand):
The paginator used to paginate the help command output.
"""
def __init__(self, **options: Any) -> None:
def __init__(self, **options: Unpack[_MinimalHelpCommandOptions]) -> None:
self.sort_commands: bool = options.pop('sort_commands', True)
self.commands_heading: str = options.pop('commands_heading', 'Commands')
self.dm_help: bool = options.pop('dm_help', False)
self.dm_help_threshold: int = options.pop('dm_help_threshold', 1000)
self.aliases_heading: str = options.pop('aliases_heading', 'Aliases:')
self.no_category: str = options.pop('no_category', 'No Category')
self.paginator: Paginator = options.pop('paginator', None)
if self.paginator is None:
paginator = options.pop('paginator', None)
if paginator is None:
self.paginator: Paginator = Paginator(suffix=None, prefix=None)
else:
self.paginator: Paginator = paginator
super().__init__(**options)

65
discord/ext/commands/hybrid.py

@ -24,19 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
List,
Tuple,
Type,
TypeVar,
Union,
Optional,
)
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Tuple, Type, TypeVar, Union, Optional
import discord
import inspect
@ -51,7 +39,7 @@ from .cog import Cog
from .view import StringView
if TYPE_CHECKING:
from typing_extensions import Self, ParamSpec, Concatenate
from typing_extensions import Self, ParamSpec, Concatenate, Unpack
from ._types import ContextT, Coro, BotT
from .bot import Bot
from .context import Context
@ -60,6 +48,29 @@ if TYPE_CHECKING:
AutocompleteCallback,
ChoiceT,
)
from .core import _CommandKwargs
class _HybridCommandKwargs(_CommandKwargs, total=False):
guild_ids: list[int]
guild_only: bool
default_permissions: bool
nsfw: bool
with_app_command: bool
class _HybridCommandDecoratorKwargs(_HybridCommandKwargs, total=False):
description: Union[str, app_commands.locale_str]
class _HybridGroupKwargs(_HybridCommandDecoratorKwargs, total=False):
with_app_command: bool
guild_ids: list[int]
guild_only: bool
default_permissions: bool
nsfw: bool
description: str
class _HybridGroupDecoratorKwargs(_HybridGroupKwargs, total=False):
description: Union[str, app_commands.locale_str]
fallback: Union[str, app_commands.locale_str]
__all__ = (
@ -256,7 +267,7 @@ def replace_parameter(
# Special case Optional[X] where X is a single type that can optionally be a converter
inner = args[0]
is_inner_transformer = is_transformer(inner)
if is_converter(inner) and not is_inner_transformer:
if (is_converter(inner) or inner in CONVERTER_MAPPING) and not is_inner_transformer:
param = param.replace(annotation=Optional[ConverterTransformer(inner, original)])
else:
raise
@ -501,7 +512,7 @@ class HybridCommand(Command[CogT, P, T]):
*,
name: Union[str, app_commands.locale_str] = MISSING,
description: Union[str, app_commands.locale_str] = MISSING,
**kwargs: Any,
**kwargs: Unpack[_HybridCommandKwargs], # type: ignore # name, description
) -> None:
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None)
if name is not MISSING:
@ -621,7 +632,7 @@ class HybridGroup(Group[CogT, P, T]):
name: Union[str, app_commands.locale_str] = MISSING,
description: Union[str, app_commands.locale_str] = MISSING,
fallback: Optional[Union[str, app_commands.locale_str]] = None,
**attrs: Any,
**attrs: Unpack[_HybridGroupKwargs], # type: ignore # name, description
) -> None:
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None)
if name is not MISSING:
@ -825,7 +836,7 @@ class HybridGroup(Group[CogT, P, T]):
name: Union[str, app_commands.locale_str] = MISSING,
*args: Any,
with_app_command: bool = True,
**kwargs: Any,
**kwargs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridCommand[CogT, P2, U]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
the internal command list via :meth:`add_command`.
@ -837,8 +848,8 @@ class HybridGroup(Group[CogT, P, T]):
"""
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
kwargs.setdefault('parent', self)
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for users to set
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result
@ -849,7 +860,7 @@ class HybridGroup(Group[CogT, P, T]):
name: Union[str, app_commands.locale_str] = MISSING,
*args: Any,
with_app_command: bool = True,
**kwargs: Any,
**kwargs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridGroup[CogT, P2, U]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
@ -861,8 +872,8 @@ class HybridGroup(Group[CogT, P, T]):
"""
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
kwargs.setdefault('parent', self)
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
kwargs.setdefault('parent', self) # type: ignore # parent is not for users to set
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func) # type: ignore # name, with_app_command
self.add_command(result)
return result
@ -873,7 +884,7 @@ def hybrid_command(
name: Union[str, app_commands.locale_str] = MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
**attrs: Unpack[_HybridCommandDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]:
r"""A decorator that transforms a function into a :class:`.HybridCommand`.
@ -916,7 +927,7 @@ def hybrid_command(
if isinstance(func, Command):
raise TypeError('Callback is already a command.')
# Pyright does not allow Command[Any] to be assigned to Command[CogT] despite it being okay here
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # name, with_app_command
return decorator
@ -925,7 +936,7 @@ def hybrid_group(
name: Union[str, app_commands.locale_str] = MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
**attrs: Unpack[_HybridGroupDecoratorKwargs], # type: ignore # name, with_app_command
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]:
"""A decorator that transforms a function into a :class:`.HybridGroup`.
@ -949,6 +960,6 @@ def hybrid_group(
def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridGroup[CogT, P, T]:
if isinstance(func, Command):
raise TypeError('Callback is already a command.')
return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs)
return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # name, with_app_command
return decorator

3
discord/ext/commands/parameters.py

@ -278,8 +278,7 @@ class ParameterAlias(Protocol):
description: str = empty,
displayed_default: str = empty,
displayed_name: str = empty,
) -> Any:
...
) -> Any: ...
param: ParameterAlias = parameter

32
discord/ext/commands/view.py

@ -31,22 +31,22 @@ from .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, Expecte
# map from opening quotes to closing quotes
_quotes = {
'"': '"',
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"«": "»",
"": "",
"": "",
"": "",
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'«': '»',
'': '',
'': '',
'': '',
}
_all_quotes = set(_quotes.keys()) | set(_quotes.values())

14
discord/file.py

@ -199,6 +199,20 @@ class File:
def filename(self, value: str) -> None:
self._filename, self.spoiler = _strip_spoiler(value)
@property
def uri(self) -> str:
""":class:`str`: Returns the ``attachment://<filename>`` URI for this file.
This is used in certain places such as embeds or components to refer
to an uploaded file via URL.
.. note::
Due to Discord's filename processing, the filename must be ASCII aphanumeric
with underscores, dashes, and periods.
.. versionadded:: 2.6
"""
return f'attachment://{self.filename}'
def reset(self, *, seek: Union[int, bool] = True) -> None:
# The `seek` parameter is needed because
# the retry-loop is iterated over multiple times

62
discord/flags.py

@ -40,12 +40,48 @@ from typing import (
Type,
TypeVar,
overload,
TypedDict,
)
from .enums import UserFlags
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
class _IntentsFlagsKwargs(TypedDict, total=False):
guilds: bool
members: bool
moderation: bool
bans: bool
emojis: bool
emojis_and_stickers: bool
expressions: bool
integrations: bool
webhooks: bool
invites: bool
voice_states: bool
presences: bool
messages: bool
guild_messages: bool
dm_messages: bool
reactions: bool
guild_reactions: bool
dm_reactions: bool
typing: bool
guild_typing: bool
dm_typing: bool
message_content: bool
guild_scheduled_events: bool
auto_moderation: bool
auto_moderation_configuration: bool
auto_moderation_execution: bool
polls: bool
guild_polls: bool
dm_polls: bool
class _MemberCacheFlagsKwargs(TypedDict, total=False):
voice: bool
joined: bool
__all__ = (
@ -76,12 +112,10 @@ class flag_value:
self.__doc__: Optional[str] = func.__doc__
@overload
def __get__(self, instance: None, owner: Type[BF]) -> Self:
...
def __get__(self, instance: None, owner: Type[BF]) -> Self: ...
@overload
def __get__(self, instance: BF, owner: Type[BF]) -> bool:
...
def __get__(self, instance: BF, owner: Type[BF]) -> bool: ...
def __get__(self, instance: Optional[BF], owner: Type[BF]) -> Any:
if instance is None:
@ -500,6 +534,16 @@ class MessageFlags(BaseFlags):
"""
return 16384
@flag_value
def components_v2(self):
""":class:`bool`: Returns ``True`` if the message has Discord's v2 components.
Does not allow sending any ``content``, ``embed``, ``embeds``, ``stickers``, or ``poll``.
.. versionadded:: 2.6
"""
return 32768
@fill_with_flags()
class PublicUserFlags(BaseFlags):
@ -755,12 +799,12 @@ class Intents(BaseFlags):
__slots__ = ()
def __init__(self, value: int = 0, **kwargs: bool) -> None:
def __init__(self, value: int = 0, **kwargs: Unpack[_IntentsFlagsKwargs]) -> None:
self.value: int = value
for key, value in kwargs.items():
for key, kwvalue in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid flag name.')
setattr(self, key, value)
setattr(self, key, kwvalue)
@classmethod
def all(cls: Type[Intents]) -> Intents:
@ -1416,7 +1460,7 @@ class MemberCacheFlags(BaseFlags):
__slots__ = ()
def __init__(self, **kwargs: bool):
def __init__(self, **kwargs: Unpack[_MemberCacheFlagsKwargs]) -> None:
bits = max(self.VALID_FLAGS.values()).bit_length()
self.value: int = (1 << bits) - 1
for key, value in kwargs.items():

8
discord/gateway.py

@ -141,7 +141,7 @@ class KeepAliveHandler(threading.Thread):
self.shard_id: Optional[int] = shard_id
self.msg: str = 'Keeping shard ID %s websocket alive with sequence %s.'
self.block_msg: str = 'Shard ID %s heartbeat blocked for more than %s seconds.'
self.behind_msg: str = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.'
self.behind_msg: str = "Can't keep up, shard ID %s websocket is %.1fs behind."
self._stop_ev: threading.Event = threading.Event()
self._last_ack: float = time.perf_counter()
self._last_send: float = time.perf_counter()
@ -152,7 +152,7 @@ class KeepAliveHandler(threading.Thread):
def run(self) -> None:
while not self._stop_ev.wait(self.interval):
if self._last_recv + self.heartbeat_timeout < time.perf_counter():
_log.warning("Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id)
_log.warning('Shard ID %s has stopped responding to the gateway. Closing and restarting.', self.shard_id)
coro = self.ws.close(4000)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
@ -160,9 +160,11 @@ class KeepAliveHandler(threading.Thread):
f.result()
except Exception:
_log.exception('An error occurred while stopping the gateway. Ignoring.')
except BaseException as exc:
_log.debug('A BaseException was raised while stopping the gateway', exc_info=exc)
finally:
self.stop()
return
return
data = self.get_payload()
_log.debug(self.msg, self.shard_id, data['d'])

143
discord/guild.py

@ -76,6 +76,7 @@ from .enums import (
AutoModRuleEventType,
ForumOrderType,
ForumLayoutType,
OnboardingMode,
)
from .mixins import Hashable
from .user import User
@ -91,6 +92,7 @@ from .sticker import GuildSticker
from .file import File
from .audit_logs import AuditLogEntry
from .object import OLDEST_OBJECT, Object
from .onboarding import Onboarding
from .welcome_screen import WelcomeScreen, WelcomeChannel
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
from .partial_emoji import _EmojiTag, PartialEmoji
@ -139,6 +141,7 @@ if TYPE_CHECKING:
from .types.widget import EditWidgetSettings
from .types.audit_log import AuditLogEvent
from .message import EmojiInputType
from .onboarding import OnboardingPrompt
VocalGuildChannel = Union[VoiceChannel, StageChannel]
GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel]
@ -216,8 +219,8 @@ class GuildPreview(Hashable):
'stickers',
'features',
'description',
"approximate_member_count",
"approximate_presence_count",
'approximate_member_count',
'approximate_presence_count',
)
def __init__(self, *, data: GuildPreviewPayload, state: ConnectionState) -> None:
@ -1293,8 +1296,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, TextChannelPayload]:
...
) -> Coroutine[Any, Any, TextChannelPayload]: ...
@overload
def _create_channel(
@ -1304,8 +1306,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, VoiceChannelPayload]:
...
) -> Coroutine[Any, Any, VoiceChannelPayload]: ...
@overload
def _create_channel(
@ -1315,8 +1316,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, StageChannelPayload]:
...
) -> Coroutine[Any, Any, StageChannelPayload]: ...
@overload
def _create_channel(
@ -1326,8 +1326,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, CategoryChannelPayload]:
...
) -> Coroutine[Any, Any, CategoryChannelPayload]: ...
@overload
def _create_channel(
@ -1337,8 +1336,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, NewsChannelPayload]:
...
) -> Coroutine[Any, Any, NewsChannelPayload]: ...
@overload
def _create_channel(
@ -1348,8 +1346,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]:
...
) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: ...
@overload
def _create_channel(
@ -1359,8 +1356,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, ForumChannelPayload]:
...
) -> Coroutine[Any, Any, ForumChannelPayload]: ...
@overload
def _create_channel(
@ -1370,8 +1366,7 @@ class Guild(Hashable):
overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, GuildChannelPayload]:
...
) -> Coroutine[Any, Any, GuildChannelPayload]: ...
def _create_channel(
self,
@ -1962,7 +1957,9 @@ class Guild(Hashable):
)
channel = ForumChannel(
state=self._state, guild=self, data=data # pyright: ignore[reportArgumentType] # it's the correct data
state=self._state,
guild=self,
data=data, # pyright: ignore[reportArgumentType] # it's the correct data
)
# temporarily add to the cache
@ -1974,11 +1971,6 @@ class Guild(Hashable):
Leaves the guild.
.. note::
You cannot leave the guild that you own, you must delete it instead
via :meth:`delete`.
Raises
--------
HTTPException
@ -1986,12 +1978,16 @@ class Guild(Hashable):
"""
await self._state.http.leave_guild(self.id)
@utils.deprecated()
async def delete(self) -> None:
"""|coro|
Deletes the guild. You must be the guild owner to delete the
guild.
.. deprecated:: 2.6
This method is deprecated and will be removed in a future version.
Raises
--------
HTTPException
@ -2091,6 +2087,9 @@ class Guild(Hashable):
owner: :class:`Member`
The new owner of the guild to transfer ownership to. Note that you must
be owner of the guild to do this.
.. deprecated:: 2.6
This parameter is deprecated and will be removed in a future version as bots can no longer own guilds.
verification_level: :class:`VerificationLevel`
The new verification level for the guild.
default_notifications: :class:`NotificationLevel`
@ -2099,6 +2098,9 @@ class Guild(Hashable):
The new explicit content filter for the guild.
vanity_code: :class:`str`
The new vanity code for the guild.
.. deprecated:: 2.6
This parameter is deprecated and will be removed in a future version as bots can no longer set this.
system_channel: Optional[:class:`TextChannel`]
The new channel that is used for the system channel. Could be ``None`` for no system channel.
system_channel_flags: :class:`SystemChannelFlags`
@ -2146,6 +2148,8 @@ class Guild(Hashable):
Note that you must be owner of the guild to do this.
.. versionadded:: 2.3
.. deprecated:: 2.6
This parameter is deprecated and will be removed in a future version as bots can no longer own guilds.
reason: Optional[:class:`str`]
The reason for editing this guild. Shows up on the audit log.
@ -3236,8 +3240,7 @@ class Guild(Hashable):
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def create_scheduled_event(
@ -3252,8 +3255,7 @@ class Guild(Hashable):
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def create_scheduled_event(
@ -3267,8 +3269,7 @@ class Guild(Hashable):
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def create_scheduled_event(
@ -3282,8 +3283,7 @@ class Guild(Hashable):
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
async def create_scheduled_event(
self,
@ -3395,7 +3395,7 @@ class Guild(Hashable):
if entity_type is None:
raise TypeError(
'invalid GuildChannel type passed, must be VoiceChannel or StageChannel ' f'not {channel.__class__.__name__}'
f'invalid GuildChannel type passed, must be VoiceChannel or StageChannel not {channel.__class__.__name__}'
)
if privacy_level is not MISSING:
@ -3650,8 +3650,7 @@ class Guild(Hashable):
mentionable: bool = ...,
secondary_colour: Optional[Union[Colour, int]] = ...,
tertiary_colour: Optional[Union[Colour, int]] = ...,
) -> Role:
...
) -> Role: ...
@overload
async def create_role(
@ -3666,8 +3665,7 @@ class Guild(Hashable):
mentionable: bool = ...,
secondary_color: Optional[Union[Colour, int]] = ...,
tertiary_color: Optional[Union[Colour, int]] = ...,
) -> Role:
...
) -> Role: ...
async def create_role(
self,
@ -4867,3 +4865,74 @@ class Guild(Hashable):
data = await self._state.http.create_soundboard_sound(self.id, reason=reason, **payload)
return SoundboardSound(guild=self, state=self._state, data=data)
async def onboarding(self) -> Onboarding:
"""|coro|
Fetches the onboarding configuration for this guild.
.. versionadded:: 2.6
Returns
--------
:class:`Onboarding`
The onboarding configuration that was fetched.
"""
data = await self._state.http.get_guild_onboarding(self.id)
return Onboarding(data=data, guild=self, state=self._state)
async def edit_onboarding(
self,
*,
prompts: List[OnboardingPrompt] = MISSING,
default_channels: List[Snowflake] = MISSING,
enabled: bool = MISSING,
mode: OnboardingMode = MISSING,
reason: str = MISSING,
) -> Onboarding:
"""|coro|
Edits the onboarding configuration for this guild.
You must have :attr:`Permissions.manage_guild` and
:attr:`Permissions.manage_roles` to do this.
.. versionadded:: 2.6
Parameters
-----------
prompts: List[:class:`OnboardingPrompt`]
The prompts that will be shown to new members.
This overrides the existing prompts and its options.
default_channels: List[:class:`abc.Snowflake`]
The channels that will be used as the default channels for new members.
This overrides the existing default channels.
enabled: :class:`bool`
Whether the onboarding configuration is enabled.
This overrides the existing enabled state.
mode: :class:`OnboardingMode`
The mode that will be used for the onboarding configuration.
reason: :class:`str`
The reason for editing the onboarding configuration. Shows up on the audit log.
Raises
-------
Forbidden
You do not have permissions to edit the onboarding configuration.
HTTPException
Editing the onboarding configuration failed.
Returns
--------
:class:`Onboarding`
The new onboarding configuration.
"""
data = await self._state.http.edit_guild_onboarding(
self.id,
prompts=[p.to_dict(id=i) for i, p in enumerate(prompts)] if prompts is not MISSING else None,
default_channel_ids=[c.id for c in default_channels] if default_channels is not MISSING else None,
enabled=enabled if enabled is not MISSING else None,
mode=mode.value if mode is not MISSING else None,
reason=reason if reason is not MISSING else None,
)
return Onboarding(data=data, guild=self, state=self._state)

98
discord/http.py

@ -57,16 +57,16 @@ from .file import File
from .mentions import AllowedMentions
from . import __version__, utils
from .utils import MISSING
from .flags import MessageFlags
_log = logging.getLogger(__name__)
if TYPE_CHECKING:
from typing_extensions import Self
from .ui.view import View
from .ui.view import BaseView
from .embeds import Embed
from .message import Attachment
from .flags import MessageFlags
from .poll import Poll
from .types import (
@ -81,6 +81,7 @@ if TYPE_CHECKING:
invite,
member,
message,
onboarding,
template,
role,
user,
@ -150,7 +151,7 @@ def handle_message_parameters(
embed: Optional[Embed] = MISSING,
embeds: Sequence[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[BaseView] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
message_reference: Optional[message.MessageReference] = MISSING,
stickers: Optional[SnowflakeList] = MISSING,
@ -193,6 +194,12 @@ def handle_message_parameters(
if view is not MISSING:
if view is not None:
payload['components'] = view.to_components()
if view.has_components_v2():
if flags is not MISSING:
flags.components_v2 = True
else:
flags = MessageFlags(components_v2=True)
else:
payload['components'] = []
@ -1040,7 +1047,7 @@ class HTTPClient:
def pin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Optional[str] = None) -> Response[None]:
r = Route(
'PUT',
'/channels/{channel_id}/pins/{message_id}',
'/channels/{channel_id}/messages/pins/{message_id}',
channel_id=channel_id,
message_id=message_id,
)
@ -1049,14 +1056,25 @@ class HTTPClient:
def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Optional[str] = None) -> Response[None]:
r = Route(
'DELETE',
'/channels/{channel_id}/pins/{message_id}',
'/channels/{channel_id}/messages/pins/{message_id}',
channel_id=channel_id,
message_id=message_id,
)
return self.request(r, reason=reason)
def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]:
return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id))
def pins_from(
self,
channel_id: Snowflake,
limit: Optional[int] = None,
before: Optional[str] = None,
) -> Response[message.ChannelPins]:
params = {}
if before is not None:
params['before'] = before
if limit is not None:
params['limit'] = limit
return self.request(Route('GET', '/channels/{channel_id}/messages/pins', channel_id=channel_id), params=params)
# Member management
@ -1863,12 +1881,10 @@ class HTTPClient:
invite_id: str,
*,
with_counts: bool = True,
with_expiration: bool = True,
guild_scheduled_event_id: Optional[Snowflake] = None,
) -> Response[invite.Invite]:
params: Dict[str, Any] = {
'with_counts': int(with_counts),
'with_expiration': int(with_expiration),
}
if guild_scheduled_event_id:
@ -2021,22 +2037,19 @@ class HTTPClient:
@overload
def get_scheduled_events(
self, guild_id: Snowflake, with_user_count: Literal[True]
) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]:
...
) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]: ...
@overload
def get_scheduled_events(
self, guild_id: Snowflake, with_user_count: Literal[False]
) -> Response[List[scheduled_event.GuildScheduledEvent]]:
...
) -> Response[List[scheduled_event.GuildScheduledEvent]]: ...
@overload
def get_scheduled_events(
self, guild_id: Snowflake, with_user_count: bool
) -> Union[
Response[List[scheduled_event.GuildScheduledEventWithUserCount]], Response[List[scheduled_event.GuildScheduledEvent]]
]:
...
]: ...
def get_scheduled_events(self, guild_id: Snowflake, with_user_count: bool) -> Response[Any]:
params = {'with_user_count': int(with_user_count)}
@ -2065,20 +2078,19 @@ class HTTPClient:
@overload
def get_scheduled_event(
self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[True]
) -> Response[scheduled_event.GuildScheduledEventWithUserCount]:
...
) -> Response[scheduled_event.GuildScheduledEventWithUserCount]: ...
@overload
def get_scheduled_event(
self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[False]
) -> Response[scheduled_event.GuildScheduledEvent]:
...
) -> Response[scheduled_event.GuildScheduledEvent]: ...
@overload
def get_scheduled_event(
self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool
) -> Union[Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent]]:
...
) -> Union[
Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent]
]: ...
def get_scheduled_event(
self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool
@ -2148,8 +2160,7 @@ class HTTPClient:
with_member: Literal[True],
before: Optional[Snowflake] = ...,
after: Optional[Snowflake] = ...,
) -> Response[scheduled_event.ScheduledEventUsersWithMember]:
...
) -> Response[scheduled_event.ScheduledEventUsersWithMember]: ...
@overload
def get_scheduled_event_users(
@ -2160,8 +2171,7 @@ class HTTPClient:
with_member: Literal[False],
before: Optional[Snowflake] = ...,
after: Optional[Snowflake] = ...,
) -> Response[scheduled_event.ScheduledEventUsers]:
...
) -> Response[scheduled_event.ScheduledEventUsers]: ...
@overload
def get_scheduled_event_users(
@ -2172,8 +2182,7 @@ class HTTPClient:
with_member: bool,
before: Optional[Snowflake] = ...,
after: Optional[Snowflake] = ...,
) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]:
...
) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]: ...
def get_scheduled_event_users(
self,
@ -2541,6 +2550,41 @@ class HTTPClient:
),
)
# Guild Onboarding
def get_guild_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]:
return self.request(Route('GET', '/guilds/{guild_id}/onboarding', guild_id=guild_id))
def edit_guild_onboarding(
self,
guild_id: Snowflake,
*,
prompts: Optional[List[onboarding.Prompt]] = None,
default_channel_ids: Optional[List[Snowflake]] = None,
enabled: Optional[bool] = None,
mode: Optional[onboarding.OnboardingMode] = None,
reason: Optional[str],
) -> Response[onboarding.Onboarding]:
payload = {}
if prompts is not None:
payload['prompts'] = prompts
if default_channel_ids is not None:
payload['default_channel_ids'] = default_channel_ids
if enabled is not None:
payload['enabled'] = enabled
if mode is not None:
payload['mode'] = mode
return self.request(
Route('PUT', f'/guilds/{guild_id}/onboarding', guild_id=guild_id),
json=payload,
reason=reason,
)
# Soundboard
def get_soundboard_default_sounds(self) -> Response[List[soundboard.SoundboardDefaultSound]]:

100
discord/interactions.py

@ -27,7 +27,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import logging
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List, overload
import asyncio
import datetime
@ -76,7 +76,7 @@ if TYPE_CHECKING:
from .mentions import AllowedMentions
from aiohttp import ClientSession
from .embeds import Embed
from .ui.view import View
from .ui.view import BaseView, View, LayoutView
from .app_commands.models import Choice, ChoiceT
from .ui.modal import Modal
from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel
@ -95,8 +95,8 @@ if TYPE_CHECKING:
GroupChannel,
]
InteractionCallbackResource = Union[
"InteractionMessage",
"InteractionCallbackActivityInstance",
'InteractionMessage',
'InteractionCallbackActivityInstance',
]
MISSING: Any = utils.MISSING
@ -482,7 +482,7 @@ class Interaction(Generic[ClientT]):
embeds: Sequence[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[Union[View, LayoutView]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
poll: Poll = MISSING,
) -> InteractionMessage:
@ -516,9 +516,15 @@ class Interaction(Generic[ClientT]):
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
.. note::
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
``None`` if the previous message had any.
poll: :class:`Poll`
The poll to create when editing the message.
@ -574,7 +580,7 @@ class Interaction(Generic[ClientT]):
# The message channel types should always match
state = _InteractionMessageState(self, self._state)
message = InteractionMessage(state=state, channel=self.channel, data=data) # type: ignore
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
self._state.store_view(view, message.id, interaction_id=self.id)
return message
@ -729,7 +735,6 @@ class InteractionCallbackResponse(Generic[ClientT]):
resource = data.get('resource')
if resource is not None:
self.type = try_enum(InteractionResponseType, resource['type'])
message = resource.get('message')
@ -898,6 +903,21 @@ class InteractionResponse(Generic[ClientT]):
)
self._response_type = InteractionResponseType.pong
@overload
async def send_message(
self,
*,
file: File = MISSING,
files: Sequence[File] = MISSING,
view: LayoutView,
ephemeral: bool = False,
allowed_mentions: AllowedMentions = MISSING,
suppress_embeds: bool = False,
silent: bool = False,
delete_after: Optional[float] = None,
) -> InteractionCallbackResponse[ClientT]: ...
@overload
async def send_message(
self,
content: Optional[Any] = None,
@ -914,6 +934,24 @@ class InteractionResponse(Generic[ClientT]):
silent: bool = False,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> InteractionCallbackResponse[ClientT]: ...
async def send_message(
self,
content: Optional[Any] = None,
*,
embed: Embed = MISSING,
embeds: Sequence[Embed] = MISSING,
file: File = MISSING,
files: Sequence[File] = MISSING,
view: BaseView = MISSING,
tts: bool = False,
ephemeral: bool = False,
allowed_mentions: AllowedMentions = MISSING,
suppress_embeds: bool = False,
silent: bool = False,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> InteractionCallbackResponse[ClientT]:
"""|coro|
@ -938,7 +976,7 @@ class InteractionResponse(Generic[ClientT]):
A list of files to upload. Must be a maximum of 10.
tts: :class:`bool`
Indicates if the message should be sent using text-to-speech.
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
The view to send with the message.
ephemeral: :class:`bool`
Indicates if the message should only be visible to the user who started the interaction.
@ -1055,7 +1093,7 @@ class InteractionResponse(Generic[ClientT]):
embed: Optional[Embed] = MISSING,
embeds: Sequence[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[Union[View, LayoutView]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
delete_after: Optional[float] = None,
suppress_embeds: bool = MISSING,
@ -1085,9 +1123,15 @@ class InteractionResponse(Generic[ClientT]):
New files will always appear after current attachments.
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
.. note::
To update the message to add a :class:`~discord.ui.LayoutView`, you
must explicitly set the ``content``, ``embed``, ``embeds``, and
``attachments`` parameters to either ``None`` or an empty array, as appropriate.
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
Controls the mentions being processed in this message. See :meth:`.Message.edit`
for more information.
@ -1169,7 +1213,7 @@ class InteractionResponse(Generic[ClientT]):
params=params,
)
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
state.store_view(view, message_id, interaction_id=original_interaction_id)
self._response_type = InteractionResponseType.message_update
@ -1382,6 +1426,17 @@ class InteractionMessage(Message):
__slots__ = ()
_state: _InteractionMessageState
@overload
async def edit(
self,
*,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: LayoutView,
allowed_mentions: Optional[AllowedMentions] = None,
delete_after: Optional[float] = None,
) -> InteractionMessage: ...
@overload
async def edit(
self,
*,
@ -1393,6 +1448,19 @@ class InteractionMessage(Message):
allowed_mentions: Optional[AllowedMentions] = None,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> InteractionMessage: ...
async def edit(
self,
*,
content: Optional[str] = MISSING,
embeds: Sequence[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[Union[View, LayoutView]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> InteractionMessage:
"""|coro|
@ -1418,9 +1486,15 @@ class InteractionMessage(Message):
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
.. note::
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
``None`` if the previous message had any.
delete_after: Optional[:class:`float`]
If provided, the number of seconds to wait in the background
before deleting the message we just sent. If the deletion fails,

7
discord/invite.py

@ -290,8 +290,6 @@ class Invite(Hashable):
+------------------------------------+--------------------------------------------------------------+
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with ``with_counts`` enabled |
+------------------------------------+--------------------------------------------------------------+
| :attr:`expires_at` | :meth:`Client.fetch_invite` with ``with_expiration`` enabled |
+------------------------------------+--------------------------------------------------------------+
If it's not in the table above then it is available by all methods.
@ -332,6 +330,9 @@ class Invite(Hashable):
:meth:`Client.fetch_invite` with ``with_expiration`` enabled, the invite will never expire.
.. versionadded:: 2.0
.. versionchanged:: 2.6
This will always be returned from all methods. ``None`` if the invite will
never expire.
channel: Optional[Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]]
The channel the invite is for.
@ -417,7 +418,7 @@ class Invite(Hashable):
target_user_data = data.get('target_user')
self.target_user: Optional[User] = None if target_user_data is None else self._state.create_user(target_user_data)
self.target_type: InviteTarget = try_enum(InviteTarget, data.get("target_type", 0))
self.target_type: InviteTarget = try_enum(InviteTarget, data.get('target_type', 0))
application = data.get('target_application')
self.target_application: Optional[PartialAppInfo] = (

180
discord/message.py

@ -96,15 +96,14 @@ if TYPE_CHECKING:
from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent
from .abc import Snowflake
from .abc import GuildChannel, MessageableChannel
from .components import ActionRow, ActionRowChildComponentType
from .components import MessageComponentType
from .state import ConnectionState
from .mentions import AllowedMentions
from .user import User
from .role import Role
from .ui.view import View
from .ui.view import View, LayoutView
EmojiInputType = Union[Emoji, PartialEmoji, str]
MessageComponentType = Union[ActionRow, ActionRowChildComponentType]
__all__ = (
@ -449,7 +448,7 @@ class DeletedReferencedMessage:
self._parent: MessageReference = parent
def __repr__(self) -> str:
return f"<DeletedReferencedMessage id={self.id} channel_id={self.channel_id} guild_id={self.guild_id!r}>"
return f'<DeletedReferencedMessage id={self.id} channel_id={self.channel_id} guild_id={self.guild_id!r}>'
@property
def id(self) -> int:
@ -489,7 +488,7 @@ class MessageSnapshot:
Extra features of the the message snapshot.
stickers: List[:class:`StickerItem`]
A list of sticker items given to the message.
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]]
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`, :class:`Container`, :class:`SectionComponent`, :class:`TextDisplay`, :class:`MediaGalleryComponent`, :class:`FileComponent`, :class:`SeparatorComponent`, :class:`ThumbnailComponent`]]
A list of components in the message.
"""
@ -533,7 +532,7 @@ class MessageSnapshot:
self.components: List[MessageComponentType] = []
for component_data in data.get('components', []):
component = _component_factory(component_data)
component = _component_factory(component_data, state) # type: ignore
if component is not None:
self.components.append(component)
@ -884,7 +883,9 @@ class MessageInteractionMetadata(Hashable):
self.modal_interaction: Optional[MessageInteractionMetadata] = None
try:
self.modal_interaction = MessageInteractionMetadata(
state=state, guild=guild, data=data['triggering_interaction_metadata'] # type: ignore # EAFP
state=state,
guild=guild,
data=data['triggering_interaction_metadata'], # type: ignore # EAFP
)
except KeyError:
pass
@ -1306,32 +1307,6 @@ class PartialMessage(Hashable):
else:
await self._state.http.delete_message(self.channel.id, self.id)
@overload
async def edit(
self,
*,
content: Optional[str] = ...,
embed: Optional[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
view: Optional[View] = ...,
) -> Message:
...
@overload
async def edit(
self,
*,
content: Optional[str] = ...,
embeds: Sequence[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
view: Optional[View] = ...,
) -> Message:
...
async def edit(
self,
*,
@ -1341,7 +1316,7 @@ class PartialMessage(Hashable):
attachments: Sequence[Union[Attachment, File]] = MISSING,
delete_after: Optional[float] = None,
allowed_mentions: Optional[AllowedMentions] = MISSING,
view: Optional[View] = MISSING,
view: Optional[Union[View, LayoutView]] = MISSING,
) -> Message:
"""|coro|
@ -1391,10 +1366,16 @@ class PartialMessage(Hashable):
are used instead.
.. versionadded:: 1.4
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
.. note::
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
``None`` if the previous message had any.
Raises
-------
HTTPException
@ -1433,8 +1414,8 @@ class PartialMessage(Hashable):
data = await self._state.http.edit_message(self.channel.id, self.id, params=params)
message = Message(state=self._state, channel=self.channel, data=data)
if view and not view.is_finished():
interaction: Optional[MessageInteraction] = getattr(self, 'interaction', None)
if view and not view.is_finished() and view.is_dispatchable():
interaction: Optional[MessageInteractionMetadata] = getattr(self, 'interaction_metadata', None)
if interaction is not None:
self._state.store_view(view, self.id, interaction_id=interaction.id)
else:
@ -1756,6 +1737,36 @@ class PartialMessage(Hashable):
return await self.guild.fetch_channel(self.id) # type: ignore # Can only be Thread in this case
@overload
async def reply(
self,
*,
file: File = ...,
view: LayoutView,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def reply(
self,
*,
files: Sequence[File] = ...,
view: LayoutView,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message: ...
@overload
async def reply(
self,
@ -1774,8 +1785,7 @@ class PartialMessage(Hashable):
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -1795,8 +1805,7 @@ class PartialMessage(Hashable):
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -1816,8 +1825,7 @@ class PartialMessage(Hashable):
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
@overload
async def reply(
@ -1837,8 +1845,7 @@ class PartialMessage(Hashable):
suppress_embeds: bool = ...,
silent: bool = ...,
poll: Poll = ...,
) -> Message:
...
) -> Message: ...
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
"""|coro|
@ -2174,6 +2181,7 @@ class Message(PartialMessage, Hashable):
'call',
'purchase_notification',
'message_snapshots',
'_pinned_at',
)
if TYPE_CHECKING:
@ -2213,6 +2221,8 @@ class Message(PartialMessage, Hashable):
self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id')
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'))
# Set by Messageable.pins
self._pinned_at: Optional[datetime.datetime] = None
self.poll: Optional[Poll] = None
try:
@ -2633,6 +2643,18 @@ class Message(PartialMessage, Hashable):
# Fall back to guild threads in case one was created after the message
return self._thread or self.guild.get_thread(self.id)
@property
def pinned_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the time
when the message was pinned.
.. note::
This is only set for messages that are returned by :meth:`abc.Messageable.pins`.
.. versionadded:: 2.6
"""
return self._pinned_at
@property
@deprecated('interaction_metadata')
def interaction(self) -> Optional[MessageInteraction]:
@ -2700,19 +2722,19 @@ class Message(PartialMessage, Hashable):
if self.type is MessageType.new_member:
formats = [
"{0} joined the party.",
"{0} is here.",
"Welcome, {0}. We hope you brought pizza.",
"A wild {0} appeared.",
"{0} just landed.",
"{0} just slid into the server.",
"{0} just showed up!",
"Welcome {0}. Say hi!",
"{0} hopped into the server.",
"Everyone welcome {0}!",
'{0} joined the party.',
'{0} is here.',
'Welcome, {0}. We hope you brought pizza.',
'A wild {0} appeared.',
'{0} just landed.',
'{0} just slid into the server.',
'{0} just showed up!',
'Welcome {0}. Say hi!',
'{0} hopped into the server.',
'Everyone welcome {0}!',
"Glad you're here, {0}.",
"Good to see you, {0}.",
"Yay you made it, {0}!",
'Good to see you, {0}.',
'Yay you made it, {0}!',
]
created_at_ms = int(self.created_at.timestamp() * 1000)
@ -2771,7 +2793,7 @@ class Message(PartialMessage, Hashable):
if self.type is MessageType.thread_starter_message:
if self.reference is None or self.reference.resolved is None:
return 'Sorry, we couldn\'t load the first message in this thread'
return "Sorry, we couldn't load the first message in this thread"
# the resolved message for the reference will be a Message
return self.reference.resolved.content # type: ignore
@ -2841,39 +2863,11 @@ class Message(PartialMessage, Hashable):
embed.fields,
name='poll_question_text',
)
return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore
return f"{self.author.display_name}'s poll {poll_title.value} has closed." # type: ignore
# Fallback for unknown message types
return ''
@overload
async def edit(
self,
*,
content: Optional[str] = ...,
embed: Optional[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
suppress: bool = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
view: Optional[View] = ...,
) -> Message:
...
@overload
async def edit(
self,
*,
content: Optional[str] = ...,
embeds: Sequence[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
suppress: bool = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
view: Optional[View] = ...,
) -> Message:
...
async def edit(
self,
*,
@ -2884,7 +2878,7 @@ class Message(PartialMessage, Hashable):
suppress: bool = False,
delete_after: Optional[float] = None,
allowed_mentions: Optional[AllowedMentions] = MISSING,
view: Optional[View] = MISSING,
view: Optional[Union[View, LayoutView]] = MISSING,
) -> Message:
"""|coro|
@ -2942,10 +2936,16 @@ class Message(PartialMessage, Hashable):
are used instead.
.. versionadded:: 1.4
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
.. note::
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
``None`` if the previous message had any.
Raises
-------
HTTPException
@ -2991,7 +2991,7 @@ class Message(PartialMessage, Hashable):
data = await self._state.http.edit_message(self.channel.id, self.id, params=params)
message = Message(state=self._state, channel=self.channel, data=data)
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
self._state.store_view(view, self.id)
if delete_after is not None:

369
discord/onboarding.py

@ -0,0 +1,369 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Union
from .mixins import Hashable
from .enums import OnboardingMode, OnboardingPromptType, try_enum
from .partial_emoji import PartialEmoji
from .utils import cached_slot_property, MISSING
from . import utils
__all__ = (
'Onboarding',
'OnboardingPrompt',
'OnboardingPromptOption',
)
if TYPE_CHECKING:
from typing_extensions import Self
from .abc import GuildChannel, Snowflake
from .emoji import Emoji
from .guild import Guild
from .partial_emoji import PartialEmoji
from .role import Role
from .threads import Thread
from .types.onboarding import (
Prompt as PromptPayload,
PromptOption as PromptOptionPayload,
CreatePromptOption as CreatePromptOptionPayload,
Onboarding as OnboardingPayload,
)
from .state import ConnectionState
class OnboardingPromptOption(Hashable):
"""Represents a onboarding prompt option.
This can be manually created for :meth:`Guild.edit_onboarding`.
.. versionadded:: 2.6
Parameters
-----------
title: :class:`str`
The title of this prompt option.
emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]
The emoji tied to this option. May be a custom emoji, or a unicode emoji. I
f this is a string, it will be converted to a :class:`PartialEmoji`.
description: Optional[:class:`str`]
The description of this prompt option.
channels: Iterable[Union[:class:`abc.Snowflake`, :class:`int`]]
The channels the user will be added to if this option is selected.
roles: Iterable[Union[:class:`abc.Snowflake`, :class:`int`]]
The roles the user will be given if this option is selected.
Attributes
-----------
id: :class:`int`
The ID of this prompt option. If this was manually created then the ID will be ``0``.
title: :class:`str`
The title of this prompt option.
description: Optional[:class:`str`]
The description of this prompt option.
emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]
The emoji tied to this option. May be a custom emoji, or a unicode emoji.
channel_ids: Set[:class:`int`]
The IDs of the channels the user will be added to if this option is selected.
role_ids: Set[:class:`int`]
The IDs of the roles the user will be given if this option is selected.
"""
__slots__ = (
'title',
'emoji',
'description',
'id',
'channel_ids',
'role_ids',
'_guild',
'_cs_channels',
'_cs_roles',
)
def __init__(
self,
*,
title: str,
emoji: Union[Emoji, PartialEmoji, str] = MISSING,
description: Optional[str] = None,
channels: Iterable[Union[Snowflake, int]] = MISSING,
roles: Iterable[Union[Snowflake, int]] = MISSING,
) -> None:
self.id: int = 0
self.title: str = title
self.description: Optional[str] = description
self.emoji: Optional[Union[Emoji, PartialEmoji]] = (
PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji if emoji is not MISSING else None
)
self.channel_ids: Set[int] = (
{c.id if not isinstance(c, int) else c for c in channels} if channels is not MISSING else set()
)
self.role_ids: Set[int] = {c.id if not isinstance(c, int) else c for c in roles} if roles is not MISSING else set()
self._guild: Optional[Guild] = None
def __repr__(self) -> str:
return f'<OnboardingPromptOption id={self.id!r} title={self.title!r} emoji={self.emoji!r}>'
@classmethod
def from_dict(cls, *, data: PromptOptionPayload, state: ConnectionState, guild: Guild) -> Self:
instance = cls(
title=data['title'],
description=data['description'],
emoji=state.get_emoji_from_partial_payload(data['emoji']) if 'emoji' in data else MISSING,
channels=[int(id) for id in data['channel_ids']],
roles=[int(id) for id in data['role_ids']],
)
instance._guild = guild
instance.id = int(data['id'])
return instance
def to_dict(
self,
) -> CreatePromptOptionPayload:
res: CreatePromptOptionPayload = {
'title': self.title,
'description': self.description,
'channel_ids': list(self.channel_ids),
'role_ids': list(self.role_ids),
}
if self.emoji:
res.update((self.emoji._to_partial())._to_onboarding_prompt_option_payload()) # type: ignore
return res
@property
def guild(self) -> Guild:
""":class:`Guild`: The guild this prompt option is related to.
Raises
-------
ValueError
If the prompt option was created manually.
"""
if self._guild is None:
raise ValueError('This prompt does not have an associated guild because it was created manually.')
return self._guild
@cached_slot_property('_cs_channels')
def channels(self) -> List[Union[GuildChannel, Thread]]:
"""List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels which will be made visible if this option is selected.
Raises
-------
ValueError
IF the prompt option is manually created, therefore has no guild.
"""
it = filter(None, map(self.guild._resolve_channel, self.channel_ids))
return utils._unique(it)
@cached_slot_property('_cs_roles')
def roles(self) -> List[Role]:
"""List[:class:`Role`]: The list of roles given to the user if this option is selected.
Raises
-------
ValueError
If the prompt option is manually created, therefore has no guild.
"""
it = filter(None, map(self.guild.get_role, self.role_ids))
return utils._unique(it)
class OnboardingPrompt:
"""Represents a onboarding prompt.
This can be manually created for :meth:`Guild.edit_onboarding`.
.. versionadded:: 2.6
Parameters
-----------
type: :class:`OnboardingPromptType`
The type of this prompt.
title: :class:`str`
The title of this prompt.
options: List[:class:`OnboardingPromptOption`]
The options of this prompt.
single_select: :class:`bool`
Whether this prompt is single select.
Defaults to ``True``.
required: :class:`bool`
Whether this prompt is required.
Defaults to ``True``.
in_onboarding: :class:`bool`
Whether this prompt is in the onboarding flow.
Defaults to ``True``.
Attributes
-----------
id: :class:`int`
The ID of this prompt. If this was manually created then the ID will be ``0``.
type: :class:`OnboardingPromptType`
The type of this prompt.
title: :class:`str`
The title of this prompt.
options: List[:class:`OnboardingPromptOption`]
The options of this prompt.
single_select: :class:`bool`
Whether this prompt is single select.
required: :class:`bool`
Whether this prompt is required.
in_onboarding: :class:`bool`
Whether this prompt is in the onboarding flow.
"""
__slots__ = (
'id',
'type',
'title',
'options',
'single_select',
'required',
'in_onboarding',
'_guild',
)
def __init__(
self,
*,
type: OnboardingPromptType,
title: str,
options: List[OnboardingPromptOption],
single_select: bool = True,
required: bool = True,
in_onboarding: bool = True,
) -> None:
self.id: int = 0
self.type: OnboardingPromptType = type
self.title: str = title
self.options: List[OnboardingPromptOption] = options
self.single_select: bool = single_select
self.required: bool = required
self.in_onboarding: bool = in_onboarding
self._guild: Optional[Guild] = None
def __repr__(self) -> str:
return f'<OnboardingPrompt id={self.id!r} title={self.title!r}, type={self.type!r}>'
@classmethod
def from_dict(cls, *, data: PromptPayload, state: ConnectionState, guild: Guild) -> Self:
instance = cls(
type=try_enum(OnboardingPromptType, data['type']),
title=data['title'],
options=[
OnboardingPromptOption.from_dict(data=option_data, state=state, guild=guild) # type: ignore
for option_data in data['options']
],
single_select=data['single_select'],
required=data['required'],
in_onboarding=data['in_onboarding'],
)
instance.id = int(data['id'])
return instance
def to_dict(self, *, id: int) -> PromptPayload:
return {
'id': id,
'type': self.type.value,
'title': self.title,
'options': [option.to_dict() for option in self.options],
'single_select': self.single_select,
'required': self.required,
'in_onboarding': self.in_onboarding,
}
@property
def guild(self) -> Guild:
""":class:`Guild`: The guild this prompt is related to.
Raises
------
ValueError
If the prompt was created manually.
"""
if self._guild is None:
raise ValueError('This prompt does not have an associated guild because it was created manually.')
return self._guild
def get_option(self, option_id: int, /) -> Optional[OnboardingPromptOption]:
"""Optional[:class:`OnboardingPromptOption`]: The option with the given ID, if found."""
return next((option for option in self.options if option.id == option_id), None)
class Onboarding:
"""Represents a guild's onboarding configuration.
.. versionadded:: 2.6
Attributes
-----------
guild: :class:`Guild`
The guild the onboarding configuration is for.
prompts: List[:class:`OnboardingPrompt`]
The list of prompts shown during the onboarding and customize community flows.
default_channel_ids: Set[:class:`int`]
The IDs of the channels exposed to a new user by default.
enabled: :class:`bool`:
Whether onboarding is enabled in this guild.
mode: :class:`OnboardingMode`
The mode of onboarding for this guild.
"""
__slots__ = (
'_state',
'_cs_default_channels',
'guild',
'prompts',
'default_channel_ids',
'enabled',
'mode',
)
def __init__(self, *, data: OnboardingPayload, guild: Guild, state: ConnectionState) -> None:
self._state: ConnectionState = state
self.guild: Guild = guild
self.default_channel_ids: Set[int] = {int(channel_id) for channel_id in data['default_channel_ids']}
self.prompts: List[OnboardingPrompt] = [
OnboardingPrompt.from_dict(data=prompt_data, state=state, guild=guild) for prompt_data in data['prompts']
]
self.enabled: bool = data['enabled']
self.mode: OnboardingMode = try_enum(OnboardingMode, data.get('mode', 0))
def __repr__(self) -> str:
return f'<Onboarding guild={self.guild!r} enabled={self.enabled!r} mode={self.mode!r}>'
@cached_slot_property('_cs_default_channels')
def default_channels(self) -> List[Union[GuildChannel, Thread]]:
"""List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels exposed to a new user by default."""
it = filter(None, map(self.guild._resolve_channel, self.default_channel_ids))
return utils._unique(it)
def get_prompt(self, prompt_id: int, /) -> Optional[OnboardingPrompt]:
"""Optional[:class:`OnboardingPrompt`]: The prompt with the given ID, if found."""
return next((prompt for prompt in self.prompts if prompt.id == prompt_id), None)

12
discord/opus.py

@ -72,7 +72,7 @@ __all__ = (
_log = logging.getLogger(__name__)
OPUS_SILENCE = b'\xF8\xFF\xFE'
OPUS_SILENCE = b'\xf8\xff\xfe'
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
@ -218,7 +218,7 @@ def libopus_loader(name: str) -> Any:
if item[3]:
func.errcheck = item[3]
except KeyError:
_log.exception("Error assigning check function to %s", func)
_log.exception('Error assigning check function to %s', func)
return lib
@ -476,16 +476,14 @@ class Decoder(_OpusStruct):
return ret.value
@overload
def decode(self, data: bytes, *, fec: bool) -> bytes:
...
def decode(self, data: bytes, *, fec: bool) -> bytes: ...
@overload
def decode(self, data: Literal[None], *, fec: Literal[False]) -> bytes:
...
def decode(self, data: Literal[None], *, fec: Literal[False]) -> bytes: ...
def decode(self, data: Optional[bytes], *, fec: bool = False) -> bytes:
if data is None and fec:
raise TypeError("Invalid arguments: FEC cannot be used with null data")
raise TypeError('Invalid arguments: FEC cannot be used with null data')
if data is None:
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME

6
discord/partial_emoji.py

@ -167,6 +167,12 @@ class PartialEmoji(_EmojiTag, AssetMixin):
return {'emoji_id': self.id, 'emoji_name': None}
return {'emoji_id': None, 'emoji_name': self.name}
def _to_onboarding_prompt_option_payload(self) -> Dict[str, Any]:
if self.id is not None:
return {'emoji_id': self.id, 'emoji_name': self.name, 'emoji_animated': self.animated}
return {'emoji_name': self.name}
@classmethod
def with_state(
cls,

108
discord/permissions.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Optional
from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Optional, TypedDict, Generic, TypeVar
from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value
__all__ = (
@ -33,7 +33,72 @@ __all__ = (
)
if TYPE_CHECKING:
from typing_extensions import Self
from typing_extensions import Self, Unpack
BoolOrNoneT = TypeVar('BoolOrNoneT', bound=Optional[bool])
class _BasePermissionsKwargs(Generic[BoolOrNoneT], TypedDict, total=False):
create_instant_invite: BoolOrNoneT
kick_members: BoolOrNoneT
ban_members: BoolOrNoneT
administrator: BoolOrNoneT
manage_channels: BoolOrNoneT
manage_guild: BoolOrNoneT
add_reactions: BoolOrNoneT
view_audit_log: BoolOrNoneT
priority_speaker: BoolOrNoneT
stream: BoolOrNoneT
read_messages: BoolOrNoneT
view_channel: BoolOrNoneT
send_messages: BoolOrNoneT
send_tts_messages: BoolOrNoneT
manage_messages: BoolOrNoneT
embed_links: BoolOrNoneT
attach_files: BoolOrNoneT
read_message_history: BoolOrNoneT
mention_everyone: BoolOrNoneT
external_emojis: BoolOrNoneT
use_external_emojis: BoolOrNoneT
view_guild_insights: BoolOrNoneT
connect: BoolOrNoneT
speak: BoolOrNoneT
mute_members: BoolOrNoneT
deafen_members: BoolOrNoneT
move_members: BoolOrNoneT
use_voice_activation: BoolOrNoneT
change_nickname: BoolOrNoneT
manage_nicknames: BoolOrNoneT
manage_roles: BoolOrNoneT
manage_permissions: BoolOrNoneT
manage_webhooks: BoolOrNoneT
manage_expressions: BoolOrNoneT
manage_emojis: BoolOrNoneT
manage_emojis_and_stickers: BoolOrNoneT
use_application_commands: BoolOrNoneT
request_to_speak: BoolOrNoneT
manage_events: BoolOrNoneT
manage_threads: BoolOrNoneT
create_public_threads: BoolOrNoneT
create_private_threads: BoolOrNoneT
send_messages_in_threads: BoolOrNoneT
external_stickers: BoolOrNoneT
use_external_stickers: BoolOrNoneT
use_embedded_activities: BoolOrNoneT
moderate_members: BoolOrNoneT
use_soundboard: BoolOrNoneT
use_external_sounds: BoolOrNoneT
send_voice_messages: BoolOrNoneT
create_expressions: BoolOrNoneT
create_events: BoolOrNoneT
send_polls: BoolOrNoneT
create_polls: BoolOrNoneT
use_external_apps: BoolOrNoneT
pin_messages: BoolOrNoneT
class _PermissionsKwargs(_BasePermissionsKwargs[bool]): ...
class _PermissionOverwriteKwargs(_BasePermissionsKwargs[Optional[bool]]): ...
# A permission alias works like a regular flag but is marked
# So the PermissionOverwrite knows to work with it
@ -135,32 +200,32 @@ class Permissions(BaseFlags):
__slots__ = ()
def __init__(self, permissions: int = 0, **kwargs: bool):
def __init__(self, permissions: int = 0, **kwargs: Unpack[_PermissionsKwargs]):
if not isinstance(permissions, int):
raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
self.value = permissions
for key, value in kwargs.items():
for key, kwvalue in kwargs.items():
try:
flag = self.VALID_FLAGS[key]
except KeyError:
raise TypeError(f'{key!r} is not a valid permission name.') from None
else:
self._set_flag(flag, value)
self._set_flag(flag, kwvalue) # type: ignore # TypedDict annoyance where kwvalue is an object instead of bool
def is_subset(self, other: Permissions) -> bool:
"""Returns ``True`` if self has the same or fewer permissions as other."""
if isinstance(other, Permissions):
return (self.value & other.value) == self.value
else:
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
raise TypeError(f'cannot compare {self.__class__.__name__} with {other.__class__.__name__}')
def is_superset(self, other: Permissions) -> bool:
"""Returns ``True`` if self has the same or more permissions as other."""
if isinstance(other, Permissions):
return (self.value | other.value) == self.value
else:
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
raise TypeError(f'cannot compare {self.__class__.__name__} with {other.__class__.__name__}')
def is_strict_subset(self, other: Permissions) -> bool:
"""Returns ``True`` if the permissions on other are a strict subset of those on self."""
@ -187,7 +252,7 @@ class Permissions(BaseFlags):
permissions set to ``True``.
"""
# Some of these are 0 because we don't want to set unnecessary bits
return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
return cls(0b0000_0000_0000_1110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
@classmethod
def _timeout_mask(cls) -> int:
@ -202,6 +267,7 @@ class Permissions(BaseFlags):
base.read_messages = True
base.send_tts_messages = False
base.manage_messages = False
base.pin_messages = True
base.create_private_threads = False
base.create_public_threads = False
base.manage_threads = False
@ -260,7 +326,7 @@ class Permissions(BaseFlags):
Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`,
:attr:`use_embedded_activities`, and :attr:`use_external_apps` permissions.
"""
return cls(0b0000_0000_0000_0110_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001)
return cls(0b0000_0000_0000_1110_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001)
@classmethod
def general(cls) -> Self:
@ -308,8 +374,11 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.4
Added :attr:`send_polls` and :attr:`use_external_apps` permissions.
.. versionchanged:: 2.7
Added :attr:`pin_messages` permission.
"""
return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000)
return cls(0b0000_0000_0000_1110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000)
@classmethod
def voice(cls) -> Self:
@ -391,7 +460,7 @@ class Permissions(BaseFlags):
"""
return cls(1 << 3)
def update(self, **kwargs: bool) -> None:
def update(self, **kwargs: Unpack[_PermissionsKwargs]) -> None:
r"""Bulk updates this permission object.
Allows you to set multiple attributes by using keyword
@ -406,7 +475,7 @@ class Permissions(BaseFlags):
for key, value in kwargs.items():
flag = self.VALID_FLAGS.get(key)
if flag is not None:
self._set_flag(flag, value)
self._set_flag(flag, value) # type: ignore
def handle_overwrite(self, allow: int, deny: int) -> None:
# Basically this is what's happening here.
@ -503,7 +572,7 @@ class Permissions(BaseFlags):
@flag_value
def manage_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
""":class:`bool`: Returns ``True`` if a user can delete messages or bypass slowmode in a text channel.
.. note::
@ -794,6 +863,14 @@ class Permissions(BaseFlags):
"""
return 1 << 50
@flag_value
def pin_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can pin messages.
.. versionadded:: 2.7
"""
return 1 << 51
def _augment_from_permissions(cls):
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
@ -917,8 +994,9 @@ class PermissionOverwrite:
send_polls: Optional[bool]
create_polls: Optional[bool]
use_external_apps: Optional[bool]
pin_messages: Optional[bool]
def __init__(self, **kwargs: Optional[bool]):
def __init__(self, **kwargs: Unpack[_PermissionOverwriteKwargs]) -> None:
self._values: Dict[str, Optional[bool]] = {}
for key, value in kwargs.items():
@ -980,7 +1058,7 @@ class PermissionOverwrite:
"""
return len(self._values) == 0
def update(self, **kwargs: Optional[bool]) -> None:
def update(self, **kwargs: Unpack[_PermissionOverwriteKwargs]) -> None:
r"""Bulk updates this permission overwrite object.
Allows you to set multiple attributes by using keyword

15
discord/player.py

@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import threading
@ -163,7 +164,7 @@ class FFmpegAudio(AudioSource):
stderr: Optional[IO[bytes]] = subprocess_kwargs.pop('stderr', None)
if stderr == subprocess.PIPE:
warnings.warn("Passing subprocess.PIPE does nothing", DeprecationWarning, stacklevel=3)
warnings.warn('Passing subprocess.PIPE does nothing', DeprecationWarning, stacklevel=3)
stderr = None
piping_stderr = False
@ -573,7 +574,7 @@ class FFmpegOpusAudio(FFmpegAudio):
if isinstance(method, str):
probefunc = getattr(cls, '_probe_codec_' + method, None)
if probefunc is None:
raise AttributeError(f"Invalid probe method {method!r}")
raise AttributeError(f'Invalid probe method {method!r}')
if probefunc is cls._probe_codec_native:
fallback = cls._probe_codec_fallback
@ -603,9 +604,9 @@ class FFmpegOpusAudio(FFmpegAudio):
except BaseException:
_log.exception("Fallback probe using '%s' failed", executable)
else:
_log.debug("Fallback probe found codec=%s, bitrate=%s", codec, bitrate)
_log.debug('Fallback probe found codec=%s, bitrate=%s', codec, bitrate)
else:
_log.debug("Probe found codec=%s, bitrate=%s", codec, bitrate)
_log.debug('Probe found codec=%s, bitrate=%s', codec, bitrate)
return codec, bitrate
@ -634,11 +635,11 @@ class FFmpegOpusAudio(FFmpegAudio):
output = out.decode('utf8')
codec = bitrate = None
codec_match = re.search(r"Stream #0.*?Audio: (\w+)", output)
codec_match = re.search(r'Stream #0.*?Audio: (\w+)', output)
if codec_match:
codec = codec_match.group(1)
br_match = re.search(r"(\d+) [kK]b/s", output)
br_match = re.search(r'(\d+) [kK]b/s', output)
if br_match:
bitrate = max(int(br_match.group(1)), 512)
@ -825,7 +826,7 @@ class AudioPlayer(threading.Thread):
try:
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.client.loop)
except Exception:
_log.exception("Speaking call in player failed")
_log.exception('Speaking call in player failed')
def send_silence(self, count: int = 5) -> None:
try:

2
discord/poll.py

@ -483,7 +483,7 @@ class Poll:
return data
def __repr__(self) -> str:
return f"<Poll duration={self.duration} question=\"{self.question}\" answers={self.answers}>"
return f'<Poll duration={self.duration} question="{self.question}" answers={self.answers}>'
@property
def question(self) -> str:

1
discord/presences.py

@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Tuple

2
discord/primary_guild.py

@ -83,7 +83,7 @@ class PrimaryGuild:
@classmethod
def _default(cls, state: ConnectionState) -> Self:
payload: PrimaryGuildPayload = {"identity_enabled": False} # type: ignore
payload: PrimaryGuildPayload = {'identity_enabled': False} # type: ignore
return cls(state=state, data=payload)
def __repr__(self) -> str:

2
discord/reaction.py

@ -102,7 +102,7 @@ class Reaction:
def __init__(self, *, message: Message, data: ReactionPayload, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None):
self.message: Message = message
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data['emoji'])
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_emoji_from_partial_payload(data['emoji'])
self.count: int = data.get('count', 1)
self.me: bool = data['me']
details = data.get('count_details', {})

18
discord/role.py

@ -429,10 +429,10 @@ class Role(Hashable):
async def _move(self, position: int, reason: Optional[str]) -> None:
if position <= 0:
raise ValueError("Cannot move role to position 0 or below")
raise ValueError('Cannot move role to position 0 or below')
if self.is_default():
raise ValueError("Cannot move default role")
raise ValueError('Cannot move default role')
if self.position == position:
return # Save discord the extra request.
@ -447,7 +447,7 @@ class Role(Hashable):
else:
roles.append(self.id)
payload: List[RolePositionUpdate] = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
payload: List[RolePositionUpdate] = [{'id': z[0], 'position': z[1]} for z in zip(roles, change_range)]
await http.move_role_position(self.guild.id, payload, reason=reason)
async def edit(
@ -599,20 +599,16 @@ class Role(Hashable):
return Role(guild=self.guild, data=data, state=self._state)
@overload
async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...):
...
async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): ...
@overload
async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...):
...
async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): ...
@overload
async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...):
...
async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): ...
@overload
async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...):
...
async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): ...
async def move(
self,

15
discord/scheduled_event.py

@ -310,8 +310,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def edit(
@ -327,8 +326,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def edit(
@ -344,8 +342,7 @@ class ScheduledEvent(Hashable):
image: bytes = ...,
location: str,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def edit(
@ -360,8 +357,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
@overload
async def edit(
@ -376,8 +372,7 @@ class ScheduledEvent(Hashable):
image: bytes = ...,
location: str,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
) -> ScheduledEvent: ...
async def edit(
self,

10
discord/shard.py

@ -52,6 +52,12 @@ if TYPE_CHECKING:
from .activity import BaseActivity
from .flags import Intents
from .types.gateway import SessionStartLimit
from .client import _ClientOptions
class _AutoShardedClientOptions(_ClientOptions, total=False):
shard_ids: List[int]
shard_connect_timeout: Optional[float]
__all__ = (
'AutoShardedClient',
@ -313,7 +319,7 @@ class SessionStartLimits:
The number of identify requests allowed per 5 seconds
"""
__slots__ = ("total", "remaining", "reset_after", "max_concurrency")
__slots__ = ('total', 'remaining', 'reset_after', 'max_concurrency')
def __init__(self, **kwargs: Unpack[SessionStartLimit]):
self.total: int = kwargs['total']
@ -365,7 +371,7 @@ class AutoShardedClient(Client):
if TYPE_CHECKING:
_connection: AutoShardedConnectionState
def __init__(self, *args: Any, intents: Intents, **kwargs: Any) -> None:
def __init__(self, *args: Any, intents: Intents, **kwargs: Unpack[_AutoShardedClientOptions]) -> None:
kwargs.pop('shard_id', None)
self.shard_ids: Optional[List[int]] = kwargs.pop('shard_ids', None)
self.shard_connect_timeout: Optional[float] = kwargs.pop('shard_connect_timeout', 180.0)

1
discord/sku.py

@ -22,7 +22,6 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import AsyncIterator, Optional, TYPE_CHECKING

4
discord/soundboard.py

@ -146,7 +146,7 @@ class SoundboardDefaultSound(BaseSoundboardSound):
('emoji', self.emoji),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f"<{self.__class__.__name__} {inner}>"
return f'<{self.__class__.__name__} {inner}>'
class SoundboardSound(BaseSoundboardSound):
@ -203,7 +203,7 @@ class SoundboardSound(BaseSoundboardSound):
('user', self.user),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f"<{self.__class__.__name__} {inner}>"
return f'<{self.__class__.__name__} {inner}>'
def _update(self, data: SoundboardSoundPayload):
super()._update(data)

21
discord/state.py

@ -71,7 +71,7 @@ from .flags import ApplicationFlags, Intents, MemberCacheFlags
from .invite import Invite
from .integrations import _integration_factory
from .interactions import Interaction
from .ui.view import ViewStore, View
from .ui.view import ViewStore, BaseView
from .scheduled_event import ScheduledEvent
from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
@ -412,12 +412,12 @@ class ConnectionState(Generic[ClientT]):
self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
return sticker
def store_view(self, view: View, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None:
def store_view(self, view: BaseView, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None:
if interaction_id is not None:
self._view_store.remove_interaction_mapping(interaction_id)
self._view_store.add_view(view, message_id)
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
def prevent_view_updates_for(self, message_id: int) -> Optional[BaseView]:
return self._view_store.remove_message_tracking(message_id)
def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
@ -427,7 +427,7 @@ class ConnectionState(Generic[ClientT]):
self._view_store.remove_dynamic_items(*items)
@property
def persistent_views(self) -> Sequence[View]:
def persistent_views(self) -> Sequence[BaseView]:
return self._view_store.persistent_views
@property
@ -1265,14 +1265,12 @@ class ConnectionState(Generic[ClientT]):
return guild.id not in self._guilds
@overload
async def chunk_guild(self, guild: Guild, *, wait: Literal[True] = ..., cache: Optional[bool] = ...) -> List[Member]:
...
async def chunk_guild(self, guild: Guild, *, wait: Literal[True] = ..., cache: Optional[bool] = ...) -> List[Member]: ...
@overload
async def chunk_guild(
self, guild: Guild, *, wait: Literal[False] = ..., cache: Optional[bool] = ...
) -> asyncio.Future[List[Member]]:
...
) -> asyncio.Future[List[Member]]: ...
async def chunk_guild(
self, guild: Guild, *, wait: bool = True, cache: Optional[bool] = None
@ -1792,7 +1790,7 @@ class ConnectionState(Generic[ClientT]):
return channel.guild.get_member(user_id)
return self.get_user(user_id)
def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[Emoji, PartialEmoji, str]:
def get_emoji_from_partial_payload(self, data: PartialEmojiPayload) -> Union[Emoji, PartialEmoji, str]:
emoji_id = utils._get_as_snowflake(data, 'id')
if not emoji_id:
@ -1803,7 +1801,10 @@ class ConnectionState(Generic[ClientT]):
return self._emojis[emoji_id]
except KeyError:
return PartialEmoji.with_state(
self, animated=data.get('animated', False), id=emoji_id, name=data['name'] # type: ignore
self,
animated=data.get('animated', False),
id=emoji_id,
name=data['name'], # type: ignore
)
def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Union[Emoji, PartialEmoji, str]:

6
discord/threads.py

@ -109,6 +109,10 @@ class Thread(Messageable, Hashable):
An approximate number of messages in this thread.
member_count: :class:`int`
An approximate number of members in this thread. This caps at 50.
total_message_sent: :class:`int`
The total number of messages sent, including deleted messages.
.. versionadded:: 2.6
me: Optional[:class:`ThreadMember`]
A thread member representing yourself, if you've joined the thread.
This could not be available.
@ -152,6 +156,7 @@ class Thread(Messageable, Hashable):
'archiver_id',
'auto_archive_duration',
'archive_timestamp',
'total_message_sent',
'_created_at',
'_flags',
'_applied_tags',
@ -185,6 +190,7 @@ class Thread(Messageable, Hashable):
self.slowmode_delay: int = data.get('rate_limit_per_user', 0)
self.message_count: int = data['message_count']
self.member_count: int = data['member_count']
self.total_message_sent: int = data.get('total_message_sent', 0)
self._flags: int = data.get('flags', 0)
# SnowflakeList is sorted, but this would not be proper for applied tags, where order actually matters.
self._applied_tags: array.array[int] = array.array('Q', map(int, data.get('applied_tags', [])))

6
discord/types/activity.py

@ -31,6 +31,7 @@ from .snowflake import Snowflake
StatusType = Literal['idle', 'dnd', 'online', 'offline']
StatusDisplayType = Literal[0, 1, 2]
class PartialPresenceUpdate(TypedDict):
@ -62,6 +63,8 @@ class ActivityAssets(TypedDict, total=False):
large_text: str
small_image: str
small_text: str
large_url: str
small_url: str
class ActivitySecrets(TypedDict, total=False):
@ -104,3 +107,6 @@ class Activity(_BaseActivity, total=False):
instance: bool
buttons: List[str]
sync_id: str
state_url: str
details_url: str
status_display_type: Optional[StatusDisplayType]

33
discord/types/audit_log.py

@ -38,6 +38,7 @@ from .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMod
from .threads import Thread
from .command import ApplicationCommand, ApplicationCommandPermissions
from .automod import AutoModerationTriggerMetadata
from .onboarding import PromptOption, Prompt
AuditLogEvent = Literal[
1,
@ -100,6 +101,13 @@ AuditLogEvent = Literal[
146,
150,
151,
163,
164,
165,
166,
167,
190,
191,
]
@ -117,6 +125,7 @@ class _AuditLogChange_Str(TypedDict):
'tags',
'unicode_emoji',
'emoji_name',
'title',
]
new_value: str
old_value: str
@ -164,6 +173,10 @@ class _AuditLogChange_Bool(TypedDict):
'available',
'archived',
'locked',
'enabled',
'single_select',
'required',
'in_onboarding',
]
new_value: bool
old_value: bool
@ -274,8 +287,8 @@ class _AuditLogChange_AppCommandPermissions(TypedDict):
old_value: ApplicationCommandPermissions
class _AuditLogChange_AppliedTags(TypedDict):
key: Literal['applied_tags']
class _AuditLogChange_SnowflakeList(TypedDict):
key: Literal['applied_tags', 'default_channel_ids']
new_value: List[Snowflake]
old_value: List[Snowflake]
@ -298,6 +311,18 @@ class _AuditLogChange_TriggerMetadata(TypedDict):
old_value: Optional[AutoModerationTriggerMetadata]
class _AuditLogChange_Prompts(TypedDict):
key: Literal['prompts']
new_value: List[Prompt]
old_value: List[Prompt]
class _AuditLogChange_Options(TypedDict):
key: Literal['options']
new_value: List[PromptOption]
old_value: List[PromptOption]
class _AuditLogChange_RoleColours(TypedDict):
key: Literal['colors']
new_value: RoleColours
@ -324,10 +349,12 @@ AuditLogChange = Union[
_AuditLogChange_Status,
_AuditLogChange_EntityType,
_AuditLogChange_AppCommandPermissions,
_AuditLogChange_AppliedTags,
_AuditLogChange_SnowflakeList,
_AuditLogChange_AvailableTags,
_AuditLogChange_DefaultReactionEmoji,
_AuditLogChange_TriggerMetadata,
_AuditLogChange_Prompts,
_AuditLogChange_Options,
_AuditLogChange_RoleColours,
]

3
discord/types/automod.py

@ -33,8 +33,7 @@ AutoModerationRuleEventType = Literal[1]
AutoModerationTriggerPresets = Literal[1, 2, 3]
class Empty(TypedDict):
...
class Empty(TypedDict): ...
class _AutoModerationActionMetadataAlert(TypedDict):

1
discord/types/channel.py

@ -126,6 +126,7 @@ class ThreadChannel(_BaseChannel):
rate_limit_per_user: int
message_count: int
member_count: int
total_message_sent: int
thread_metadata: ThreadMetadata
member: NotRequired[ThreadMember]
owner_id: NotRequired[Snowflake]

2
discord/types/command.py

@ -163,7 +163,7 @@ class _ChatInputApplicationCommand(_BaseApplicationCommand, total=False):
class _BaseContextMenuApplicationCommand(_BaseApplicationCommand):
description: Literal[""]
description: Literal['']
class _UserApplicationCommand(_BaseContextMenuApplicationCommand):

104
discord/types/components.py

@ -24,24 +24,31 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import List, Literal, TypedDict, Union
from typing import List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .emoji import PartialEmoji
from .channel import ChannelType
ComponentType = Literal[1, 2, 3, 4]
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18]
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
TextStyle = Literal[1, 2]
DefaultValueType = Literal['user', 'role', 'channel']
SeparatorSpacing = Literal[1, 2]
MediaItemLoadingState = Literal[0, 1, 2, 3]
class ActionRow(TypedDict):
class ComponentBase(TypedDict):
id: NotRequired[int]
type: int
class ActionRow(ComponentBase):
type: Literal[1]
components: List[ActionRowChildComponent]
class ButtonComponent(TypedDict):
class ButtonComponent(ComponentBase):
type: Literal[2]
style: ButtonStyle
custom_id: NotRequired[str]
@ -60,7 +67,7 @@ class SelectOption(TypedDict):
emoji: NotRequired[PartialEmoji]
class SelectComponent(TypedDict):
class SelectComponent(ComponentBase):
custom_id: str
placeholder: NotRequired[str]
min_values: NotRequired[int]
@ -99,11 +106,11 @@ class ChannelSelectComponent(SelectComponent):
default_values: NotRequired[List[SelectDefaultValues]]
class TextInput(TypedDict):
class TextInput(ComponentBase):
type: Literal[4]
custom_id: str
style: TextStyle
label: str
label: Optional[str]
placeholder: NotRequired[str]
value: NotRequired[str]
required: NotRequired[bool]
@ -113,10 +120,91 @@ class TextInput(TypedDict):
class SelectMenu(SelectComponent):
type: Literal[3, 5, 6, 7, 8]
required: NotRequired[bool] # Only for StringSelect within modals
options: NotRequired[List[SelectOption]]
channel_types: NotRequired[List[ChannelType]]
default_values: NotRequired[List[SelectDefaultValues]]
class SectionComponent(ComponentBase):
type: Literal[9]
components: List[Union[TextComponent, ButtonComponent]]
accessory: Component
class TextComponent(ComponentBase):
type: Literal[10]
content: str
class UnfurledMediaItem(TypedDict):
url: str
proxy_url: str
height: NotRequired[Optional[int]]
width: NotRequired[Optional[int]]
content_type: NotRequired[str]
placeholder: str
loading_state: MediaItemLoadingState
attachment_id: NotRequired[int]
flags: NotRequired[int]
class ThumbnailComponent(ComponentBase):
type: Literal[11]
media: UnfurledMediaItem
description: NotRequired[Optional[str]]
spoiler: NotRequired[bool]
class MediaGalleryItem(TypedDict):
media: UnfurledMediaItem
description: NotRequired[str]
spoiler: NotRequired[bool]
class MediaGalleryComponent(ComponentBase):
type: Literal[12]
items: List[MediaGalleryItem]
class FileComponent(ComponentBase):
type: Literal[13]
file: UnfurledMediaItem
spoiler: NotRequired[bool]
name: NotRequired[str]
size: NotRequired[int]
class SeparatorComponent(ComponentBase):
type: Literal[14]
divider: NotRequired[bool]
spacing: NotRequired[SeparatorSpacing]
class ContainerComponent(ComponentBase):
type: Literal[17]
accent_color: NotRequired[int]
spoiler: NotRequired[bool]
components: List[ContainerChildComponent]
class LabelComponent(ComponentBase):
type: Literal[18]
label: str
description: NotRequired[str]
component: Union[StringSelectComponent, TextInput]
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
Component = Union[ActionRow, ActionRowChildComponent]
ContainerChildComponent = Union[
ActionRow,
TextComponent,
MediaGalleryComponent,
FileComponent,
SectionComponent,
SectionComponent,
ContainerComponent,
SeparatorComponent,
ThumbnailComponent,
]
Component = Union[ActionRowChildComponent, LabelComponent, ContainerChildComponent]

1
discord/types/gateway.py

@ -145,6 +145,7 @@ class InviteCreateEvent(TypedDict):
code: str
created_at: str
max_age: int
expires_at: Optional[str]
max_uses: int
temporary: bool
uses: Literal[0]

8
discord/types/guild.py

@ -94,6 +94,8 @@ GuildFeature = Literal[
'RAID_ALERTS_DISABLED',
'SOUNDBOARD',
'MORE_SOUNDBOARD',
'GUESTS_ENABLED',
'GUILD_TAGS',
]
@ -114,8 +116,7 @@ class _GuildPreviewUnique(TypedDict):
approximate_presence_count: int
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique):
...
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique): ...
class Guild(_BaseGuildPreview):
@ -165,8 +166,7 @@ class InviteGuild(Guild, total=False):
welcome_screen: WelcomeScreen
class GuildWithCounts(Guild, _GuildPreviewUnique):
...
class GuildWithCounts(Guild, _GuildPreviewUnique): ...
class GuildPrune(TypedDict):

25
discord/types/interactions.py

@ -78,6 +78,14 @@ class PartialThread(_BasePartialChannel):
type: ThreadType
thread_metadata: ThreadMetadata
parent_id: Snowflake
applied_tags: NotRequired[List[Snowflake]]
owner_id: Snowflake
message_count: int
member_count: int
rate_limit_per_user: int
last_message_id: NotRequired[Optional[Snowflake]]
flags: NotRequired[int]
total_message_sent: int
class ResolvedData(TypedDict, total=False):
@ -202,7 +210,13 @@ class ModalSubmitTextInputInteractionData(TypedDict):
value: str
ModalSubmitComponentItemInteractionData = ModalSubmitTextInputInteractionData
class ModalSubmitStringSelectInteractionData(TypedDict):
type: Literal[3]
custom_id: str
values: List[str]
ModalSubmitComponentItemInteractionData = Union[ModalSubmitTextInputInteractionData, ModalSubmitStringSelectInteractionData]
class ModalSubmitActionRowInteractionData(TypedDict):
@ -210,7 +224,14 @@ class ModalSubmitActionRowInteractionData(TypedDict):
components: List[ModalSubmitComponentItemInteractionData]
ModalSubmitComponentInteractionData = Union[ModalSubmitActionRowInteractionData, ModalSubmitComponentItemInteractionData]
class ModalSubmitLabelInteractionData(TypedDict):
type: Literal[18]
component: ModalSubmitComponentItemInteractionData
ModalSubmitComponentInteractionData = Union[
ModalSubmitLabelInteractionData, ModalSubmitActionRowInteractionData, ModalSubmitComponentItemInteractionData
]
class ModalSubmitInteractionData(TypedDict):

6
discord/types/invite.py

@ -44,7 +44,6 @@ class _InviteMetadata(TypedDict, total=False):
max_age: int
temporary: bool
created_at: str
expires_at: Optional[str]
class VanityInvite(_InviteMetadata):
@ -66,16 +65,17 @@ class Invite(IncompleteInvite, total=False):
guild_scheduled_event: GuildScheduledEvent
type: InviteType
flags: NotRequired[int]
expires_at: Optional[str]
class InviteWithCounts(Invite, _GuildPreviewUnique):
...
class InviteWithCounts(Invite, _GuildPreviewUnique): ...
class GatewayInviteCreate(TypedDict):
channel_id: Snowflake
code: str
created_at: str
expires_at: Optional[str]
max_age: int
max_uses: int
temporary: bool

16
discord/types/message.py

@ -33,7 +33,7 @@ from .user import User
from .emoji import PartialEmoji
from .embed import Embed
from .channel import ChannelType
from .components import Component
from .components import ComponentBase
from .interactions import MessageInteraction, MessageInteractionMetadata
from .sticker import StickerItem
from .threads import Thread
@ -189,7 +189,7 @@ class MessageSnapshot(TypedDict):
mentions: List[UserWithMember]
mention_roles: SnowflakeList
sticker_items: NotRequired[List[StickerItem]]
components: NotRequired[List[Component]]
components: NotRequired[List[ComponentBase]]
class Message(PartialMessage):
@ -221,7 +221,7 @@ class Message(PartialMessage):
referenced_message: NotRequired[Optional[Message]]
interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata
interaction_metadata: NotRequired[MessageInteractionMetadata]
components: NotRequired[List[Component]]
components: NotRequired[List[ComponentBase]]
position: NotRequired[int]
role_subscription_data: NotRequired[RoleSubscriptionData]
thread: NotRequired[Thread]
@ -237,3 +237,13 @@ class AllowedMentions(TypedDict):
roles: SnowflakeList
users: SnowflakeList
replied_user: bool
class MessagePin(TypedDict):
pinned_at: str
message: Message
class ChannelPins(TypedDict):
items: List[MessagePin]
has_more: bool

72
discord/types/onboarding.py

@ -0,0 +1,72 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal, Optional, TypedDict, List, Union
from .emoji import PartialEmoji
from .snowflake import Snowflake
if TYPE_CHECKING:
from typing_extensions import NotRequired
PromptType = Literal[0, 1]
OnboardingMode = Literal[0, 1]
class _PromptOption(TypedDict):
channel_ids: List[Snowflake]
role_ids: List[Snowflake]
title: str
description: Optional[str]
class CreatePromptOption(_PromptOption):
emoji_id: NotRequired[Snowflake]
emoji_name: NotRequired[str]
emoji_animated: NotRequired[bool]
class PromptOption(_PromptOption):
id: Snowflake
emoji: NotRequired[PartialEmoji]
class Prompt(TypedDict):
id: Snowflake
options: List[Union[PromptOption, CreatePromptOption]]
title: str
single_select: bool
required: bool
in_onboarding: bool
type: PromptType
class Onboarding(TypedDict):
guild_id: Snowflake
prompts: List[Prompt]
default_channel_ids: List[Snowflake]
enabled: bool
mode: OnboardingMode

9
discord/types/scheduled_event.py

@ -81,16 +81,13 @@ class _WithUserCount(TypedDict):
user_count: int
class _StageInstanceScheduledEventWithUserCount(StageInstanceScheduledEvent, _WithUserCount):
...
class _StageInstanceScheduledEventWithUserCount(StageInstanceScheduledEvent, _WithUserCount): ...
class _VoiceScheduledEventWithUserCount(VoiceScheduledEvent, _WithUserCount):
...
class _VoiceScheduledEventWithUserCount(VoiceScheduledEvent, _WithUserCount): ...
class _ExternalScheduledEventWithUserCount(ExternalScheduledEvent, _WithUserCount):
...
class _ExternalScheduledEventWithUserCount(ExternalScheduledEvent, _WithUserCount): ...
GuildScheduledEventWithUserCount = Union[

1
discord/types/threads.py

@ -60,6 +60,7 @@ class Thread(TypedDict):
type: ThreadType
member_count: int
message_count: int
total_message_sent: int
rate_limit_per_user: int
thread_metadata: ThreadMetadata
member: NotRequired[ThreadMember]

3
discord/types/webhook.py

@ -63,5 +63,4 @@ class _FullWebhook(TypedDict, total=False):
application_id: Optional[Snowflake]
class Webhook(PartialWebhook, _FullWebhook):
...
class Webhook(PartialWebhook, _FullWebhook): ...

9
discord/ui/__init__.py

@ -16,3 +16,12 @@ from .button import *
from .select import *
from .text_input import *
from .dynamic import *
from .container import *
from .file import *
from .media_gallery import *
from .section import *
from .separator import *
from .text_display import *
from .thumbnail import *
from .action_row import *
from .label import *

588
discord/ui/action_row.py

@ -0,0 +1,588 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
List,
Literal,
Optional,
Sequence,
Type,
TypeVar,
Union,
overload,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .button import Button, button as _button
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
from ..components import ActionRow as ActionRowComponent
from ..enums import ButtonStyle, ComponentType, ChannelType
from ..partial_emoji import PartialEmoji
from ..utils import MISSING, get as _utils_get
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from .select import (
BaseSelectT,
ValidDefaultValues,
MentionableSelectT,
ChannelSelectT,
RoleSelectT,
UserSelectT,
SelectT,
)
from ..emoji import Emoji
from ..components import SelectOption
from ..interactions import Interaction
from .container import Container
SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT]
S = TypeVar('S', bound=Union['ActionRow', 'Container', 'LayoutView'], covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('ActionRow',)
class _ActionRowCallback:
__slots__ = ('row', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], row: ActionRow, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.row: ActionRow = row
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.row, interaction, self.item)
class ActionRow(Item[V]):
r"""Represents a UI action row.
This is a top-level layout component that can only be used on :class:`LayoutView`
and can contain :class:`Button`\s and :class:`Select`\s in it.
Action rows can only have 5 children. This can be inherited.
.. versionadded:: 2.6
Examples
--------
.. code-block:: python3
import discord
from discord import ui
# you can subclass it and add components with the decorators
class MyActionRow(ui.ActionRow):
@ui.button(label='Click Me!')
async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked me!')
# or use it directly on LayoutView
class MyView(ui.LayoutView):
row = ui.ActionRow()
# or you can use your subclass:
# row = MyActionRow()
# you can add items with row.button and row.select
@row.button(label='A button!')
async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked a button!')
Parameters
----------
\*children: :class:`Item`
The initial children of this action row.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__action_row_children_items__: ClassVar[List[ItemCallbackType[Self, Any]]] = []
__discord_ui_action_row__: ClassVar[bool] = True
__item_repr_attributes__ = ('id',)
def __init__(
self,
*children: Item[V],
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = self._init_children()
self._children.extend(children)
self._weight: int = sum(i.width for i in self._children)
if self._weight > 5:
raise ValueError('maximum number of children exceeded')
self.id = id
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Self, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if len(children) > 5:
raise TypeError('ActionRow cannot have more than 5 children')
cls.__action_row_children_items__ = list(children.values())
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)}>'
def _init_children(self) -> List[Item[Any]]:
children = []
for func in self.__action_row_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ActionRowCallback(func, self, item) # type: ignore
item._parent = getattr(func, '__discord_ui_parent__', self)
setattr(self, func.__name__, item)
children.append(item)
return children
def _update_view(self, view) -> None:
self._view = view
for child in self._children:
child._view = view
def _has_children(self):
return True
def _is_v2(self) -> bool:
# although it is not really a v2 component the only usecase here is for
# LayoutView which basically represents the top-level payload of components
# and ActionRow is only allowed there anyways.
# If the user tries to add any V2 component to a View instead of LayoutView
# it should error anyways.
return True
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.action_row]:
return ComponentType.action_row
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The list of children attached to this action row."""
return self._children.copy()
def walk_children(self) -> Generator[Item[V], Any, None]:
"""An iterator that recursively walks through all the children of this action row
and its children, if applicable.
Yields
------
:class:`Item`
An item in the action row.
"""
for child in self.children:
yield child
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this action row."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self._children if isinstance(item, TextDisplay))
def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to this action row.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to add to the action row.
Raises
------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (5)
or (40) for the entire view.
"""
if (self._weight + item.width) > 5:
raise ValueError('maximum number of children exceeded')
if len(self._children) >= 5:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if self._view:
self._view._add_count(1)
item._update_view(self.view)
item._parent = self
self._weight += 1
self._children.append(item)
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from the action row.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the action row.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
self._view._add_count(-1)
self._weight -= 1
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all items from the action row.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(self._children))
self._children.clear()
self._weight = 0
return self
def to_component_dict(self) -> Dict[str, Any]:
components = []
for component in self.children:
components.append(component.to_component_dict())
base = {
'type': self.type.value,
'components': components,
}
if self.id is not None:
base['id'] = self.id
return base
def button(
self,
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
id: Optional[int] = None,
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
"""A decorator that attaches a button to the action row.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
the :class:`discord.ui.Button` being pressed.
.. note::
Buttons with a URL or a SKU cannot be created with this function.
Consider creating a :class:`Button` manually and adding it via
:meth:`ActionRow.add_item` instead. This is beacuse these buttons
cannot have a callback associated with them since Discord does not
do any processing with them.
Parameters
----------
label: Optional[:class:`str`]
The label of the button, if any.
Can only be up to 80 characters.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
It is recommended to not set this parameters to prevent conflicts.
Can only be up to 100 characters.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled or not. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
ret = _button(
label=label,
custom_id=custom_id,
disabled=disabled,
style=style,
emoji=emoji,
row=None,
id=id,
)(func)
ret.__discord_ui_parent__ = self # type: ignore
return ret # type: ignore
return decorator # type: ignore
@overload
def select(
self,
*,
cls: Type[SelectT] = Select[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, SelectT]: ...
@overload
def select(
self,
*,
cls: Type[UserSelectT] = UserSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, UserSelectT]: ...
@overload
def select(
self,
*,
cls: Type[RoleSelectT] = RoleSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, RoleSelectT]: ...
@overload
def select(
self,
*,
cls: Type[ChannelSelectT] = ChannelSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, ChannelSelectT]: ...
@overload
def select(
self,
*,
cls: Type[MentionableSelectT] = MentionableSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, MentionableSelectT]: ...
def select(
self,
*,
cls: Type[BaseSelectT] = Select[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING,
placeholder: Optional[str] = None,
custom_id: str = MISSING,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> SelectCallbackDecorator[S, BaseSelectT]:
"""A decorator that attaches a select menu to the action row.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
the chosen select class.
To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values
will depend on the type of select menu used. View the table below for more information.
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
| Select Type | Resolved Values |
+========================================+=================================================================================================================+
| :class:`discord.ui.Select` | List[:class:`str`] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
| :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
| :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
| :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
| :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
Example
---------
.. code-block:: python3
class MyView(discord.ui.LayoutView):
action_row = discord.ui.ActionRow()
@action_row.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text])
async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect):
return await interaction.response.send_message(f'You selected {select.values[0].mention}')
Parameters
------------
cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \
Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]]
The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other
select types to display different select menus to the user. See the table above for the different
values you can get from each select type. Subclasses work as well, however the callback in the subclass will
get overridden.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
Can only be up to 150 characters.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
Can only be up to 100 characters.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu. This can only be used with
:class:`Select` instances.
Can only contain up to 25 items.
channel_types: List[:class:`~discord.ChannelType`]
The types of channels to show in the select menu. Defaults to all channels. This can only be used
with :class:`ChannelSelect` instances.
disabled: :class:`bool`
Whether the select is disabled or not. Defaults to ``False``.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances.
If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor.
Number of items must be in range of ``min_values`` and ``max_values``.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[S, BaseSelectT]) -> ItemCallbackType[S, BaseSelectT]:
r = _select( # type: ignore
cls=cls, # type: ignore
placeholder=placeholder,
custom_id=custom_id,
min_values=min_values,
max_values=max_values,
options=options,
channel_types=channel_types,
disabled=disabled,
default_values=default_values,
id=id,
)(func)
r.__discord_ui_parent__ = self
return r
return decorator # type: ignore
@classmethod
def from_component(cls, component: ActionRowComponent) -> ActionRow:
from .view import _component_to_item
self = cls(id=component.id)
for cmp in component.children:
self.add_item(_component_to_item(cmp, self))
return self

67
discord/ui/button.py

@ -24,12 +24,13 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import copy
from typing import Callable, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
import inspect
import os
from .item import Item, ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..components import Button as ButtonComponent
@ -42,11 +43,14 @@ __all__ = (
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import BaseView
from .action_row import ActionRow
from .container import Container
from ..emoji import Emoji
from ..types.components import ButtonComponent as ButtonComponentPayload
V = TypeVar('V', bound='View', covariant=True)
S = TypeVar('S', bound='Union[BaseView, Container, ActionRow]', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class Button(Item[V]):
@ -77,11 +81,19 @@ class Button(Item[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
sku_id: Optional[:class:`int`]
The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji``
nor ``custom_id``.
.. versionadded:: 2.4
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
.. versionadded:: 2.6
"""
__item_repr_attributes__: Tuple[str, ...] = (
@ -92,6 +104,7 @@ class Button(Item[V]):
'emoji',
'row',
'sku_id',
'id',
)
def __init__(
@ -105,6 +118,7 @@ class Button(Item[V]):
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
sku_id: Optional[int] = None,
id: Optional[int] = None,
):
super().__init__()
if custom_id is not None and (url is not None or sku_id is not None):
@ -143,9 +157,19 @@ class Button(Item[V]):
style=style,
emoji=emoji,
sku_id=sku_id,
id=id,
)
self.row = row
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this button."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def style(self) -> ButtonStyle:
""":class:`discord.ButtonStyle`: The style of the button."""
@ -242,6 +266,7 @@ class Button(Item[V]):
emoji=button.emoji,
row=None,
sku_id=button.sku_id,
id=button.id,
)
@property
@ -262,6 +287,28 @@ class Button(Item[V]):
def _refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
def copy(self) -> Self:
new = copy.copy(self)
custom_id = self.custom_id
if self.custom_id is not None and not self._provided_custom_id:
custom_id = os.urandom(16).hex()
new._underlying = ButtonComponent._raw_construct(
custom_id=custom_id,
url=self.url,
disabled=self.disabled,
label=self.label,
style=self.style,
emoji=self.emoji,
sku_id=self.sku_id,
id=self.id,
)
return new
def __deepcopy__(self, memo) -> Self:
return self.copy()
def button(
*,
@ -271,7 +318,8 @@ def button(
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]:
id: Optional[int] = None,
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
"""A decorator that attaches a button to a component.
The function being decorated should have three parameters, ``self`` representing
@ -308,9 +356,17 @@ def button(
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]:
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
if not inspect.iscoroutinefunction(func):
raise TypeError('button function must be a coroutine function')
@ -324,6 +380,7 @@ def button(
'emoji': emoji,
'row': row,
'sku_id': None,
'id': id,
}
return func

375
discord/ui/container.py

@ -0,0 +1,375 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import copy
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Coroutine,
Dict,
Generator,
List,
Literal,
Optional,
TypeVar,
Union,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .view import _component_to_item, LayoutView
from ..enums import ComponentType
from ..utils import get as _utils_get
from ..colour import Colour, Color
if TYPE_CHECKING:
from typing_extensions import Self
from ..components import Container as ContainerComponent
from ..interactions import Interaction
S = TypeVar('S', bound='Container', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Container',)
class _ContainerCallback:
__slots__ = ('container', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], container: Container, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.container: Container = container
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.container, interaction, self.item)
class Container(Item[V]):
r"""Represents a UI container.
This is a top-level layout component that can only be used on :class:`LayoutView`
and can contain :class:`ActionRow`\s, :class:`TextDisplay`\s, :class:`Section`\s,
:class:`MediaGallery`\s, :class:`File`\s, and :class:`Separator`\s in it.
This can be inherited.
.. versionadded:: 2.6
Examples
--------
.. code-block:: python3
import discord
from discord import ui
# you can subclass it and add components as you would add them
# in a LayoutView
class MyContainer(ui.Container):
action_row = ui.ActionRow()
@action_row.button(label='A button in a container!')
async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked a button!')
# or use it directly on LayoutView
class MyView(ui.LayoutView):
container = ui.Container(ui.TextDisplay('I am a text display on a container!'))
# or you can use your subclass:
# container = MyContainer()
Parameters
----------
\*children: :class:`Item`
The initial children of this container.
accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]]
The colour of the container. Defaults to ``None``.
accent_color: Optional[Union[:class:`.Colour`, :class:`int`]]
The color of the container. Defaults to ``None``.
spoiler: :class:`bool`
Whether to flag this container as a spoiler. Defaults
to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]]] = {}
__discord_ui_container__: ClassVar[bool] = True
__item_repr_attributes__ = (
'accent_colour',
'spoiler',
'id',
)
def __init__(
self,
*children: Item[V],
accent_colour: Optional[Union[Colour, int]] = None,
accent_color: Optional[Union[Color, int]] = None,
spoiler: bool = False,
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = self._init_children()
for child in children:
self.add_item(child)
self.spoiler: bool = spoiler
self._colour = accent_colour if accent_colour is not None else accent_color
self.id = id
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)}>'
def _init_children(self) -> List[Item[Any]]:
children = []
parents = {}
for name, raw in self.__container_children_items__.items():
if isinstance(raw, Item):
item = raw.copy()
item._parent = self
setattr(self, name, item)
children.append(item)
parents[raw] = item
else:
# action rows can be created inside containers, and then callbacks can exist here
# so we create items based off them
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ContainerCallback(raw, self, item) # type: ignore
setattr(self, raw.__name__, item)
# this should not fail because in order for a function to be here it should be from
# an action row and must have passed the check in __init_subclass__, but still
# guarding it
parent = getattr(raw, '__discord_ui_parent__', None)
if parent is None:
raise ValueError(f'{raw.__name__} is not a valid item for a Container')
parents.get(parent, parent)._children.append(item)
# we do not append it to the children list because technically these buttons and
# selects are not from the container but the action row itself.
return children
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if isinstance(member, Item):
children[name] = member
if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
children[name] = copy.copy(member)
cls.__container_children_items__ = children
def _update_view(self, view) -> bool:
self._view = view
for child in self._children:
child._update_view(view)
return True
def _has_children(self):
return True
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The children of this container."""
return self._children.copy()
@children.setter
def children(self, value: List[Item[V]]) -> None:
self._children = value
@property
def accent_colour(self) -> Optional[Union[Colour, int]]:
"""Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``."""
return self._colour
@accent_colour.setter
def accent_colour(self, value: Optional[Union[Colour, int]]) -> None:
if value is not None and not isinstance(value, (int, Colour)):
raise TypeError(f'expected an int, or Colour, not {value.__class__.__name__!r}')
self._colour = value
accent_color = accent_colour
@property
def type(self) -> Literal[ComponentType.container]:
return ComponentType.container
@property
def width(self):
return 5
def _is_v2(self) -> bool:
return True
def to_components(self) -> List[Dict[str, Any]]:
components = []
for i in self._children:
components.append(i.to_component_dict())
return components
def to_component_dict(self) -> Dict[str, Any]:
components = self.to_components()
colour = None
if self._colour:
colour = self._colour if isinstance(self._colour, int) else self._colour.value
base = {
'type': self.type.value,
'accent_color': colour,
'spoiler': self.spoiler,
'components': components,
}
if self.id is not None:
base['id'] = self.id
return base
@classmethod
def from_component(cls, component: ContainerComponent) -> Self:
self = cls(
accent_colour=component.accent_colour,
spoiler=component.spoiler,
id=component.id,
)
self._children = [_component_to_item(cmp, self) for cmp in component.children]
return self
def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this container
and its children, if applicable.
Yields
------
:class:`Item`
An item in the container.
"""
for child in self.children:
yield child
if child._has_children():
yield from child.walk_children() # type: ignore
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this container."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self.walk_children() if isinstance(item, TextDisplay))
def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to this container.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to append.
Raises
------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (40) for the entire view.
"""
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if item._has_children() and self._view:
self._view._add_count(len(tuple(item.walk_children()))) # type: ignore
elif self._view:
self._view._add_count(1)
self._children.append(item)
item._update_view(self.view)
item._parent = self
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from this container.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the container.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
if item._has_children():
self._view._add_count(-len(tuple(item.walk_children()))) # type: ignore
else:
self._view._add_count(-1)
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all the items from the container.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(tuple(self.walk_children())))
self._children.clear()
return self

15
discord/ui/dynamic.py

@ -38,14 +38,12 @@ if TYPE_CHECKING:
from ..interactions import Interaction
from ..components import Component
from ..enums import ComponentType
from .view import View
V = TypeVar('V', bound='View', covariant=True, default=View)
from .view import View, LayoutView
else:
V = TypeVar('V', bound='View', covariant=True)
View = LayoutView = Any
class DynamicItem(Generic[BaseT], Item['View']):
class DynamicItem(Generic[BaseT], Item[Union[View, LayoutView]]):
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
that ``custom_id``.
@ -57,9 +55,10 @@ class DynamicItem(Generic[BaseT], Item['View']):
and should not be used long term. Their only purpose is to act as a "template"
for the actual dispatched item.
When this item is generated, :attr:`view` is set to a regular :class:`View` instance
from the original message given from the interaction. This means that custom view
subclasses cannot be accessed from this item.
When this item is generated, :attr:`view` is set to a regular :class:`View` instance,
but to a :class:`LayoutView` if the component was sent with one, this is obtained from
the original message given from the interaction. This means that custom view subclasses
cannot be accessed from this item.
.. versionadded:: 2.4

159
discord/ui/file.py

@ -0,0 +1,159 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union
from .item import Item
from ..components import FileComponent, UnfurledMediaItem
from ..enums import ComponentType
from ..utils import MISSING
from ..file import File as SendableFile
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('File',)
class File(Item[V]):
"""Represents a UI file component.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Example
-------
.. code-block:: python3
import discord
from discord import ui
class MyView(ui.LayoutView):
file = ui.File('attachment://file.txt')
# attachment://file.txt points to an attachment uploaded alongside this view
Parameters
----------
media: Union[:class:`str`, :class:`.UnfurledMediaItem`, :class:`discord.File`]
This file's media. If this is a string it must point to a local
file uploaded within the parent view of this item, and must
meet the ``attachment://<filename>`` format.
spoiler: :class:`bool`
Whether to flag this file as a spoiler. Defaults to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__item_repr_attributes__ = (
'media',
'spoiler',
'id',
)
def __init__(
self,
media: Union[str, UnfurledMediaItem, SendableFile],
*,
spoiler: bool = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
if isinstance(media, SendableFile):
self._underlying = FileComponent._raw_construct(
media=UnfurledMediaItem(media.uri),
spoiler=media.spoiler if spoiler is MISSING else spoiler,
id=id,
)
else:
self._underlying = FileComponent._raw_construct(
media=UnfurledMediaItem(media) if isinstance(media, str) else media,
spoiler=bool(spoiler),
id=id,
)
self.id = id
def _is_v2(self):
return True
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.file]:
return self._underlying.type
@property
def media(self) -> UnfurledMediaItem:
""":class:`.UnfurledMediaItem`: Returns this file media."""
return self._underlying.media
@media.setter
def media(self, value: Union[str, SendableFile, UnfurledMediaItem]) -> None:
if isinstance(value, str):
self._underlying.media = UnfurledMediaItem(value)
elif isinstance(value, UnfurledMediaItem):
self._underlying.media = value
elif isinstance(value, SendableFile):
self._underlying.media = UnfurledMediaItem(value.uri)
else:
raise TypeError(f'expected a str or UnfurledMediaItem or File, not {value.__class__.__name__!r}')
@property
def url(self) -> str:
""":class:`str`: Returns this file's url."""
return self._underlying.media.url
@url.setter
def url(self, value: str) -> None:
self._underlying.media = UnfurledMediaItem(value)
@property
def spoiler(self) -> bool:
""":class:`bool`: Returns whether this file should be flagged as a spoiler."""
return self._underlying.spoiler
@spoiler.setter
def spoiler(self, value: bool) -> None:
self._underlying.spoiler = value
def to_component_dict(self):
return self._underlying.to_dict()
@classmethod
def from_component(cls, component: FileComponent) -> Self:
return cls(
media=component.media,
spoiler=component.spoiler,
id=component.id,
)

77
discord/ui/item.py

@ -24,7 +24,8 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar
import copy
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Union, Tuple, Type, TypeVar
from ..interactions import Interaction
from .._types import ClientT
@ -36,13 +37,20 @@ __all__ = (
# fmt: on
if TYPE_CHECKING:
from typing_extensions import Self
from ..enums import ComponentType
from .view import View
from .view import BaseView
from ..components import Component
from .action_row import ActionRow
from .container import Container
I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
ContainerType = Union['BaseView', 'ActionRow', 'Container']
C = TypeVar('C', bound=ContainerType, covariant=True)
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
ContainedItemCallbackType = Callable[[C, Interaction[Any], I], Coroutine[Any, Any, Any]]
class Item(Generic[V]):
@ -53,11 +61,20 @@ class Item(Generic[V]):
- :class:`discord.ui.Button`
- :class:`discord.ui.Select`
- :class:`discord.ui.TextInput`
- :class:`discord.ui.ActionRow`
- :class:`discord.ui.Container`
- :class:`discord.ui.File`
- :class:`discord.ui.MediaGallery`
- :class:`discord.ui.Section`
- :class:`discord.ui.Separator`
- :class:`discord.ui.TextDisplay`
- :class:`discord.ui.Thumbnail`
- :class:`discord.ui.Label`
.. versionadded:: 2.0
"""
__item_repr_attributes__: Tuple[str, ...] = ('row',)
__item_repr_attributes__: Tuple[str, ...] = ('row', 'id')
def __init__(self):
self._view: Optional[V] = None
@ -70,6 +87,8 @@ class Item(Generic[V]):
# actually affect the intended purpose of this check because from_component is
# only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False
self._id: Optional[int] = None
self._parent: Optional[Item] = None
def to_component_dict(self) -> Dict[str, Any]:
raise NotImplementedError
@ -80,6 +99,9 @@ class Item(Generic[V]):
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
return None
def _is_v2(self) -> bool:
return False
@classmethod
def from_component(cls: Type[I], component: Component) -> I:
return cls()
@ -92,7 +114,9 @@ class Item(Generic[V]):
return False
def is_persistent(self) -> bool:
return self._provided_custom_id
if self.is_dispatchable():
return self._provided_custom_id
return True
def __repr__(self) -> str:
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
@ -104,6 +128,10 @@ class Item(Generic[V]):
@row.setter
def row(self, value: Optional[int]) -> None:
if self._is_v2():
# row is ignored on v2 components
return
if value is None:
self._row = None
elif 5 > value >= 0:
@ -117,9 +145,43 @@ class Item(Generic[V]):
@property
def view(self) -> Optional[V]:
"""Optional[:class:`View`]: The underlying view for this item."""
"""Optional[Union[:class:`View`, :class:`LayoutView`]]: The underlying view for this item."""
return self._view
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._id
@id.setter
def id(self, value: Optional[int]) -> None:
self._id = value
@property
def parent(self) -> Optional[Item[V]]:
"""Optional[:class:`Item`]: This item's parent, if applicable. Only available on items with children.
.. versionadded:: 2.6
"""
return self._parent
async def _run_checks(self, interaction: Interaction[ClientT]) -> bool:
can_run = await self.interaction_check(interaction)
if can_run and self._parent:
can_run = await self._parent._run_checks(interaction)
return can_run
def _update_view(self, view) -> None:
self._view = view
def copy(self) -> Self:
return copy.deepcopy(self)
def _has_children(self) -> bool:
return False
async def callback(self, interaction: Interaction[ClientT]) -> Any:
"""|coro|
@ -148,7 +210,8 @@ class Item(Generic[V]):
.. note::
If an exception occurs within the body then the check
is considered a failure and :meth:`discord.ui.View.on_error` is called.
is considered a failure and :meth:`View.on_error`
(or :meth:`LayoutView.on_error`) is called.
For :class:`~discord.ui.DynamicItem` this does not call the ``on_error``
handler.

140
discord/ui/label.py

@ -0,0 +1,140 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Generator, Literal, Optional, Tuple, TypeVar
from ..components import LabelComponent
from ..enums import ComponentType
from ..utils import MISSING
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..types.components import LabelComponent as LabelComponentPayload
from .view import View
# fmt: off
__all__ = (
'Label',
)
# fmt: on
V = TypeVar('V', bound='View', covariant=True)
class Label(Item[V]):
"""Represents a UI label within a modal.
.. versionadded:: 2.6
Parameters
------------
text: :class:`str`
The text to display above the input field.
Can only be up to 45 characters.
description: Optional[:class:`str`]
The description text to display right below the label text.
Can only be up to 100 characters.
component: Union[:class:`discord.ui.TextInput`, :class:`discord.ui.Select`]
The component to display below the label.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
Attributes
------------
text: :class:`str`
The text to display above the input field.
Can only be up to 45 characters.
description: Optional[:class:`str`]
The description text to display right below the label text.
Can only be up to 100 characters.
component: :class:`Item`
The component to display below the label. Currently only
supports :class:`TextInput` and :class:`Select`.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'text',
'description',
'component',
)
def __init__(
self,
*,
text: str,
component: Item[V],
description: Optional[str] = None,
id: Optional[int] = None,
) -> None:
super().__init__()
self.component: Item[V] = component
self.text: str = text
self.description: Optional[str] = description
self.id = id
@property
def width(self) -> int:
return 5
def _has_children(self) -> bool:
return True
def walk_children(self) -> Generator[Item[V], None, None]:
yield self.component
def to_component_dict(self) -> LabelComponentPayload:
payload: LabelComponentPayload = {
'type': ComponentType.label.value,
'label': self.text,
'component': self.component.to_component_dict(), # type: ignore
}
if self.description:
payload['description'] = self.description
if self.id is not None:
payload['id'] = self.id
return payload
@classmethod
def from_component(cls, component: LabelComponent) -> Self:
from .view import _component_to_item
self = cls(
text=component.label,
component=MISSING,
description=component.description,
)
self.component = _component_to_item(component.component, self)
return self
@property
def type(self) -> Literal[ComponentType.label]:
return ComponentType.label
def is_dispatchable(self) -> bool:
return False

263
discord/ui/media_gallery.py

@ -0,0 +1,263 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar, Union
from .item import Item
from ..enums import ComponentType
from ..utils import MISSING
from ..file import File
from ..components import (
MediaGalleryItem,
MediaGalleryComponent,
UnfurledMediaItem,
)
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('MediaGallery',)
class MediaGallery(Item[V]):
r"""Represents a UI media gallery.
Can contain up to 10 :class:`.MediaGalleryItem`\s.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
\*items: :class:`.MediaGalleryItem`
The initial items of this gallery.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__item_repr_attributes__ = (
'items',
'id',
)
def __init__(
self,
*items: MediaGalleryItem,
id: Optional[int] = None,
) -> None:
super().__init__()
self._underlying = MediaGalleryComponent._raw_construct(
items=list(items),
id=id,
)
def __repr__(self) -> str:
return f'<{self.__class__.__name__} items={len(self._underlying.items)}>'
@property
def items(self) -> List[MediaGalleryItem]:
"""List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items."""
return self._underlying.items.copy()
@items.setter
def items(self, value: List[MediaGalleryItem]) -> None:
if len(value) > 10:
raise ValueError('media gallery only accepts up to 10 items')
self._underlying.items = value
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
def to_component_dict(self):
return self._underlying.to_dict()
def _is_v2(self) -> bool:
return True
def add_item(
self,
*,
media: Union[str, File, UnfurledMediaItem],
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
) -> Self:
"""Adds an item to this gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
media: Union[:class:`str`, :class:`discord.File`, :class:`.UnfurledMediaItem`]
The media item data. This can be a string representing a local
file uploaded as an attachment in the message, which can be accessed
using the ``attachment://<filename>`` format, or an arbitrary url.
description: Optional[:class:`str`]
The description to show within this item. Up to 256 characters. Defaults
to ``None``.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler. Defaults to ``False``.
Raises
------
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
item = MediaGalleryItem(media, description=description, spoiler=spoiler)
self._underlying.items.append(item)
return self
def append_item(self, item: MediaGalleryItem) -> Self:
"""Appends an item to this gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`.MediaGalleryItem`
The item to add to the gallery.
Raises
------
TypeError
A :class:`.MediaGalleryItem` was not passed.
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
if not isinstance(item, MediaGalleryItem):
raise TypeError(f'expected MediaGalleryItem, not {item.__class__.__name__!r}')
self._underlying.items.append(item)
return self
def insert_item_at(
self,
index: int,
*,
media: Union[str, File, UnfurledMediaItem],
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
) -> Self:
"""Inserts an item before a specified index to the media gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
index: :class:`int`
The index of where to insert the field.
media: Union[:class:`str`, :class:`discord.File`, :class:`.UnfurledMediaItem`]
The media item data. This can be a string representing a local
file uploaded as an attachment in the message, which can be accessed
using the ``attachment://<filename>`` format, or an arbitrary url.
description: Optional[:class:`str`]
The description to show within this item. Up to 256 characters. Defaults
to ``None``.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler. Defaults to ``False``.
Raises
------
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
item = MediaGalleryItem(
media,
description=description,
spoiler=spoiler,
)
self._underlying.items.insert(index, item)
return self
def remove_item(self, item: MediaGalleryItem) -> Self:
"""Removes an item from the gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`.MediaGalleryItem`
The item to remove from the gallery.
"""
try:
self._underlying.items.remove(item)
except ValueError:
pass
return self
def clear_items(self) -> Self:
"""Removes all items from the gallery.
This function returns the class instance to allow for fluent-style
chaining.
"""
self._underlying.items.clear()
return self
@property
def type(self) -> Literal[ComponentType.media_gallery]:
return self._underlying.type
@property
def width(self):
return 5
@classmethod
def from_component(cls, component: MediaGalleryComponent) -> Self:
return cls(
*component.items,
id=component.id,
)

35
discord/ui/modal.py

@ -34,6 +34,7 @@ from ..utils import MISSING, find
from .._types import ClientT
from .item import Item
from .view import View
from .label import Label
if TYPE_CHECKING:
from typing_extensions import Self
@ -170,10 +171,12 @@ class Modal(View):
for component in components:
if component['type'] == 1:
self._refresh(interaction, component['components'])
elif component['type'] == 18:
self._refresh(interaction, [component['component']])
else:
item = find(lambda i: i.custom_id == component['custom_id'], self._children) # type: ignore
item = find(lambda i: getattr(i, 'custom_id', None) == component['custom_id'], self.walk_children()) # type: ignore
if item is None:
_log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id'])
_log.debug('Modal interaction referencing unknown item custom_id %s. Discarding', component['custom_id'])
continue
item._refresh_state(interaction, component) # type: ignore
@ -194,10 +197,34 @@ class Modal(View):
# In the future, maybe this will require checking if we set an error response.
self.stop()
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for child in children:
if isinstance(child, Label):
components.append(child.to_component_dict()) # type: ignore
else:
# Every implicit child wrapped in an ActionRow in a modal
# has a single child of width 5
# It's also deprecated to use ActionRow in modals
components.append(
{
'type': 1,
'components': [child.to_component_dict()],
}
)
return components
def _dispatch_submit(
self, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]
) -> None:
asyncio.create_task(self._scheduled_task(interaction, components), name=f'discord-ui-modal-dispatch-{self.id}')
) -> asyncio.Task[None]:
return asyncio.create_task(
self._scheduled_task(interaction, components), name=f'discord-ui-modal-dispatch-{self.id}'
)
def to_dict(self) -> Dict[str, Any]:
payload = {

261
discord/ui/section.py

@ -0,0 +1,261 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar
from .item import Item
from .text_display import TextDisplay
from ..enums import ComponentType
from ..utils import MISSING, get as _utils_get
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from ..components import SectionComponent
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Section',)
class Section(Item[V]):
r"""Represents a UI section.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
\*children: Union[:class:`str`, :class:`TextDisplay`]
The text displays of this section. Up to 3.
accessory: :class:`Item`
The section accessory.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
Attributes
----------
accessory: :class:`Item`
The section accessory.
"""
__item_repr_attributes__ = (
'accessory',
'id',
)
__discord_ui_section__: ClassVar[bool] = True
__slots__ = (
'_children',
'accessory',
)
def __init__(
self,
*children: Union[Item[V], str],
accessory: Item[V],
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = []
if children:
if len(children) > 3:
raise ValueError('maximum number of children exceeded')
self._children.extend(
[c if isinstance(c, Item) else TextDisplay(c) for c in children],
)
self.accessory: Item[V] = accessory
self.id = id
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)}>'
@property
def type(self) -> Literal[ComponentType.section]:
return ComponentType.section
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The list of children attached to this section."""
return self._children.copy()
@property
def width(self):
return 5
def _is_v2(self) -> bool:
return True
def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this section
and its children, if applicable. This includes the `accessory`.
Yields
------
:class:`Item`
An item in this section.
"""
for child in self.children:
yield child
yield self.accessory
def _update_view(self, view) -> None:
self._view = view
self.accessory._view = view
for child in self._children:
child._view = view
def _has_children(self):
return True
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this section."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self._children if isinstance(item, TextDisplay))
def add_item(self, item: Union[str, Item[Any]]) -> Self:
"""Adds an item to this section.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: Union[:class:`str`, :class:`Item`]
The item to append, if it is a string it automatically wrapped around
:class:`TextDisplay`.
Raises
------
TypeError
An :class:`Item` or :class:`str` was not passed.
ValueError
Maximum number of children has been exceeded (3) or (40)
for the entire view.
"""
if len(self._children) >= 3:
raise ValueError('maximum number of children exceeded (3)')
if not isinstance(item, (Item, str)):
raise TypeError(f'expected Item or str not {item.__class__.__name__}')
if self._view:
self._view._add_count(1)
item = item if isinstance(item, Item) else TextDisplay(item)
item._update_view(self.view)
item._parent = self
self._children.append(item)
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from this section.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the section.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
self._view._add_count(-1)
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all the items from the section.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(self._children)) # we don't count the accessory because it is required
self._children.clear()
return self
@classmethod
def from_component(cls, component: SectionComponent) -> Self:
from .view import _component_to_item
# using MISSING as accessory so we can create the new one with the parent set
self = cls(id=component.id, accessory=MISSING)
self.accessory = _component_to_item(component.accessory, self)
self.id = component.id
self._children = [_component_to_item(c, self) for c in component.children]
return self
def to_components(self) -> List[Dict[str, Any]]:
components = []
for component in self._children:
components.append(component.to_component_dict())
return components
def to_component_dict(self) -> Dict[str, Any]:
data = {
'type': self.type.value,
'components': self.to_components(),
'accessory': self.accessory.to_component_dict(),
}
if self.id is not None:
data['id'] = self.id
return data

132
discord/ui/select.py

@ -42,7 +42,7 @@ from contextvars import ContextVar
import inspect
import os
from .item import Item, ItemCallbackType
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from ..enums import ChannelType, ComponentType, SelectDefaultValueType
from ..partial_emoji import PartialEmoji
from ..emoji import Emoji
@ -72,7 +72,8 @@ __all__ = (
if TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeGuard
from .view import View
from .view import BaseView
from .action_row import ActionRow
from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import SelectMessageComponentInteractionData
from ..app_commands import AppCommandChannel, AppCommandThread
@ -101,14 +102,15 @@ if TYPE_CHECKING:
Thread,
]
V = TypeVar('V', bound='View', covariant=True)
S = TypeVar('S', bound='Union[BaseView, ActionRow]', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
SelectT = TypeVar('SelectT', bound='Select[Any]')
UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]')
RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]')
ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]')
MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]')
SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT]
SelectCallbackDecorator: TypeAlias = Callable[['ItemCallbackType[S, BaseSelectT]'], BaseSelectT]
DefaultSelectComponentTypes = Literal[
ComponentType.user_select,
ComponentType.role_select,
@ -216,6 +218,7 @@ class BaseSelect(Item[V]):
'min_values',
'max_values',
'disabled',
'id',
)
__component_attributes__: Tuple[str, ...] = (
'custom_id',
@ -223,6 +226,7 @@ class BaseSelect(Item[V]):
'min_values',
'max_values',
'disabled',
'id',
)
def __init__(
@ -235,9 +239,11 @@ class BaseSelect(Item[V]):
min_values: Optional[int] = None,
max_values: Optional[int] = None,
disabled: bool = False,
required: bool = False,
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING,
default_values: Sequence[SelectDefaultValue] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
@ -252,14 +258,25 @@ class BaseSelect(Item[V]):
min_values=min_values,
max_values=max_values,
disabled=disabled,
required=required,
channel_types=[] if channel_types is MISSING else channel_types,
options=[] if options is MISSING else options,
default_values=[] if default_values is MISSING else default_values,
id=id,
)
self.row = row
self._values: List[PossibleValue] = []
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this select."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def values(self) -> List[PossibleValue]:
values = selected_values.get({})
@ -317,6 +334,18 @@ class BaseSelect(Item[V]):
def disabled(self, value: bool) -> None:
self._underlying.disabled = bool(value)
@property
def required(self) -> bool:
""":class:`bool`: Whether the select is required or not. Only supported in modals.
.. versionadded:: 2.6
"""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = bool(value)
@property
def width(self) -> int:
return 5
@ -332,11 +361,12 @@ class BaseSelect(Item[V]):
payload: List[PossibleValue]
try:
resolved = Namespace._get_resolved_items(
interaction, data['resolved'] # pyright: ignore[reportTypedDictNotRequiredAccess]
interaction,
data['resolved'], # pyright: ignore[reportTypedDictNotRequiredAccess]
)
payload = list(resolved.values())
except KeyError:
payload = data.get("values", []) # type: ignore
payload = data.get('values', []) # type: ignore
self._values = values[self.custom_id] = payload
selected_values.set(values)
@ -384,12 +414,24 @@ class Select(BaseSelect[V]):
Can only contain up to 25 items.
disabled: :class:`bool`
Whether the select is disabled or not.
required: :class:`bool`
Whether the select is required. Only applicable within modals.
.. versionadded:: 2.6
row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('options',)
@ -403,7 +445,9 @@ class Select(BaseSelect[V]):
max_values: int = 1,
options: List[SelectOption] = MISSING,
disabled: bool = False,
required: bool = True,
row: Optional[int] = None,
id: Optional[int] = None,
) -> None:
super().__init__(
self.type,
@ -412,8 +456,10 @@ class Select(BaseSelect[V]):
min_values=min_values,
max_values=max_values,
disabled=disabled,
required=required,
options=options,
row=row,
id=id,
)
@property
@ -545,6 +591,14 @@ class UserSelect(BaseSelect[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
@ -559,6 +613,7 @@ class UserSelect(BaseSelect[V]):
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__(
self.type,
@ -569,6 +624,7 @@ class UserSelect(BaseSelect[V]):
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
id=id,
)
@property
@ -637,6 +693,14 @@ class RoleSelect(BaseSelect[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
@ -651,6 +715,7 @@ class RoleSelect(BaseSelect[V]):
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__(
self.type,
@ -661,6 +726,7 @@ class RoleSelect(BaseSelect[V]):
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
id=id,
)
@property
@ -725,6 +791,14 @@ class MentionableSelect(BaseSelect[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
@ -739,6 +813,7 @@ class MentionableSelect(BaseSelect[V]):
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__(
self.type,
@ -749,6 +824,7 @@ class MentionableSelect(BaseSelect[V]):
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
id=id,
)
@property
@ -819,6 +895,14 @@ class ChannelSelect(BaseSelect[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__component_attributes__ = BaseSelect.__component_attributes__ + (
@ -837,6 +921,7 @@ class ChannelSelect(BaseSelect[V]):
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__(
self.type,
@ -848,6 +933,7 @@ class ChannelSelect(BaseSelect[V]):
row=row,
channel_types=channel_types,
default_values=_handle_select_defaults(default_values, self.type),
id=id,
)
@property
@ -899,8 +985,8 @@ def select(
max_values: int = ...,
disabled: bool = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, SelectT]:
...
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, SelectT]: ...
@overload
@ -916,8 +1002,8 @@ def select(
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, UserSelectT]:
...
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, UserSelectT]: ...
@overload
@ -933,8 +1019,8 @@ def select(
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, RoleSelectT]:
...
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, RoleSelectT]: ...
@overload
@ -950,8 +1036,8 @@ def select(
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, ChannelSelectT]:
...
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, ChannelSelectT]: ...
@overload
@ -967,8 +1053,8 @@ def select(
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, MentionableSelectT]:
...
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, MentionableSelectT]: ...
def select(
@ -983,7 +1069,8 @@ def select(
disabled: bool = False,
default_values: Sequence[ValidDefaultValues] = MISSING,
row: Optional[int] = None,
) -> SelectCallbackDecorator[V, BaseSelectT]:
id: Optional[int] = None,
) -> SelectCallbackDecorator[S, BaseSelectT]:
"""A decorator that attaches a select menu to a component.
The function being decorated should have three parameters, ``self`` representing
@ -1041,6 +1128,10 @@ def select(
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25.
@ -1062,9 +1153,13 @@ def select(
Number of items must be in range of ``min_values`` and ``max_values``.
.. versionadded:: 2.4
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]:
def decorator(func: ItemCallbackType[S, BaseSelectT]) -> ItemCallbackType[S, BaseSelectT]:
if not inspect.iscoroutinefunction(func):
raise TypeError('select function must be a coroutine function')
callback_cls = getattr(cls, '__origin__', cls)
@ -1080,6 +1175,7 @@ def select(
'min_values': min_values,
'max_values': max_values,
'disabled': disabled,
'id': id,
}
if issubclass(callback_cls, Select):
func.__discord_ui_model_kwargs__['options'] = options

125
discord/ui/separator.py

@ -0,0 +1,125 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal, Optional, TypeVar
from .item import Item
from ..components import SeparatorComponent
from ..enums import SeparatorSpacing, ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Separator',)
class Separator(Item[V]):
"""Represents a UI separator.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
visible: :class:`bool`
Whether this separator is visible. On the client side this
is whether a divider line should be shown or not.
spacing: :class:`.SeparatorSpacing`
The spacing of this separator.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = ('_underlying',)
__item_repr_attributes__ = (
'visible',
'spacing',
'id',
)
def __init__(
self,
*,
visible: bool = True,
spacing: SeparatorSpacing = SeparatorSpacing.small,
id: Optional[int] = None,
) -> None:
super().__init__()
self._underlying = SeparatorComponent._raw_construct(
spacing=spacing,
visible=visible,
id=id,
)
self.id = id
def _is_v2(self):
return True
@property
def visible(self) -> bool:
""":class:`bool`: Whether this separator is visible.
On the client side this is whether a divider line should
be shown or not.
"""
return self._underlying.visible
@visible.setter
def visible(self, value: bool) -> None:
self._underlying.visible = value
@property
def spacing(self) -> SeparatorSpacing:
""":class:`.SeparatorSpacing`: The spacing of this separator."""
return self._underlying.spacing
@spacing.setter
def spacing(self, value: SeparatorSpacing) -> None:
self._underlying.spacing = value
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.separator]:
return self._underlying.type
def to_component_dict(self):
return self._underlying.to_dict()
@classmethod
def from_component(cls, component: SeparatorComponent) -> Self:
return cls(
visible=component.visible,
spacing=component.spacing,
id=component.id,
)

90
discord/ui/text_display.py

@ -0,0 +1,90 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal, Optional, TypeVar
from .item import Item
from ..components import TextDisplay as TextDisplayComponent
from ..enums import ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('TextDisplay',)
class TextDisplay(Item[V]):
"""Represents a UI text display.
This is a top-level layout component that can only be used on :class:`LayoutView` or :class:`Section`.
.. versionadded:: 2.6
Parameters
----------
content: :class:`str`
The content of this text display. Up to 4000 characters.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = ('content',)
def __init__(self, content: str, *, id: Optional[int] = None) -> None:
super().__init__()
self.content: str = content
self.id = id
def to_component_dict(self):
base = {
'type': self.type.value,
'content': self.content,
}
if self.id is not None:
base['id'] = self.id
return base
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.text_display]:
return ComponentType.text_display
def _is_v2(self) -> bool:
return True
@classmethod
def from_component(cls, component: TextDisplayComponent) -> Self:
return cls(
content=component.content,
id=component.id,
)

27
discord/ui/text_input.py

@ -29,7 +29,7 @@ from typing import TYPE_CHECKING, Literal, Optional, Tuple, TypeVar
from ..components import TextInput as TextInputComponent
from ..enums import ComponentType, TextStyle
from ..utils import MISSING
from ..utils import MISSING, deprecated
from .item import Item
if TYPE_CHECKING:
@ -63,9 +63,15 @@ class TextInput(Item[V]):
Parameters
------------
label: :class:`str`
label: Optional[:class:`str`]
The label to display above the text input.
Can only be up to 45 characters.
.. deprecated:: 2.6
This parameter is deprecated, use :class:`discord.ui.Label` instead.
.. versionchanged:: 2.6
This parameter is now optional and defaults to ``None``.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
If not given then one is generated for you.
@ -92,18 +98,23 @@ class TextInput(Item[V]):
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__item_repr_attributes__: Tuple[str, ...] = (
'label',
'placeholder',
'required',
'id',
)
def __init__(
self,
*,
label: str,
label: Optional[str] = None,
style: TextStyle = TextStyle.short,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
@ -112,6 +123,7 @@ class TextInput(Item[V]):
min_length: Optional[int] = None,
max_length: Optional[int] = None,
row: Optional[int] = None,
id: Optional[int] = None,
) -> None:
super().__init__()
self._value: Optional[str] = default
@ -129,8 +141,10 @@ class TextInput(Item[V]):
required=required,
min_length=min_length,
max_length=max_length,
id=id,
)
self.row = row
self.id = id
def __str__(self) -> str:
return self.value
@ -158,12 +172,14 @@ class TextInput(Item[V]):
return self._value or ''
@property
def label(self) -> str:
@deprecated('discord.ui.Label')
def label(self) -> Optional[str]:
""":class:`str`: The label of the text input."""
return self._underlying.label
@label.setter
def label(self, value: str) -> None:
@deprecated('discord.ui.Label')
def label(self, value: Optional[str]) -> None:
self._underlying.label = value
@property
@ -241,6 +257,7 @@ class TextInput(Item[V]):
min_length=component.min_length,
max_length=component.max_length,
row=None,
id=component.id,
)
@property

144
discord/ui/thumbnail.py

@ -0,0 +1,144 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union
from .item import Item
from ..enums import ComponentType
from ..components import UnfurledMediaItem
from ..file import File
from ..utils import MISSING
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from ..components import ThumbnailComponent
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Thumbnail',)
class Thumbnail(Item[V]):
"""Represents a UI Thumbnail. This currently can only be used as a :class:`Section`\'s accessory.
.. versionadded:: 2.6
Parameters
----------
media: Union[:class:`str`, :class:`discord.File`, :class:`discord.UnfurledMediaItem`]
The media of the thumbnail. This can be a URL or a reference
to an attachment that matches the ``attachment://filename.extension``
structure.
description: Optional[:class:`str`]
The description of this thumbnail. Up to 256 characters. Defaults to ``None``.
spoiler: :class:`bool`
Whether to flag this thumbnail as a spoiler. Defaults to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = (
'_media',
'description',
'spoiler',
)
__item_repr_attributes__ = (
'media',
'description',
'spoiler',
'row',
'id',
)
def __init__(
self,
media: Union[str, File, UnfurledMediaItem],
*,
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
if isinstance(media, File):
description = description if description is not MISSING else media.description
spoiler = spoiler if spoiler is not MISSING else media.spoiler
media = media.uri
self._media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
self.description: Optional[str] = None if description is MISSING else description
self.spoiler: bool = bool(spoiler)
self.id = id
@property
def width(self):
return 5
@property
def media(self) -> UnfurledMediaItem:
""":class:`discord.UnfurledMediaItem`: This thumbnail unfurled media data."""
return self._media
@media.setter
def media(self, value: Union[str, File, UnfurledMediaItem]) -> None:
if isinstance(value, str):
self._media = UnfurledMediaItem(value)
elif isinstance(value, UnfurledMediaItem):
self._media = value
elif isinstance(value, File):
self._media = UnfurledMediaItem(value.uri)
else:
raise TypeError(f'expected a str or UnfurledMediaItem, not {value.__class__.__name__!r}')
@property
def type(self) -> Literal[ComponentType.thumbnail]:
return ComponentType.thumbnail
def _is_v2(self) -> bool:
return True
def to_component_dict(self) -> Dict[str, Any]:
base = {
'type': self.type.value,
'spoiler': self.spoiler,
'media': self.media.to_dict(),
'description': self.description,
}
if self.id is not None:
base['id'] = self.id
return base
@classmethod
def from_component(cls, component: ThumbnailComponent) -> Self:
return cls(
media=component.media.url,
description=component.description,
spoiler=component.spoiler,
id=component.id,
)

582
discord/ui/view.py

@ -23,7 +23,24 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type
from typing import (
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
Iterator,
List,
Optional,
Sequence,
TYPE_CHECKING,
Set,
Tuple,
Type,
Union,
)
from functools import partial
from itertools import groupby
@ -32,6 +49,7 @@ import logging
import sys
import time
import os
from .item import Item, ItemCallbackType
from .select import Select
from .dynamic import DynamicItem
@ -41,11 +59,21 @@ from ..components import (
_component_factory,
Button as ButtonComponent,
SelectMenu as SelectComponent,
SectionComponent,
TextDisplay as TextDisplayComponent,
MediaGalleryComponent,
FileComponent,
SeparatorComponent,
ThumbnailComponent,
Container as ContainerComponent,
LabelComponent,
)
from ..utils import get as _utils_get, find as _utils_find
# fmt: off
__all__ = (
'View',
'LayoutView',
)
# fmt: on
@ -56,11 +84,13 @@ if TYPE_CHECKING:
from ..interactions import Interaction
from ..message import Message
from ..types.components import Component as ComponentPayload
from ..types.components import ComponentBase as ComponentBasePayload
from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload
from ..state import ConnectionState
from .modal import Modal
ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]]
_log = logging.getLogger(__name__)
@ -69,21 +99,65 @@ def _walk_all_components(components: List[Component]) -> Iterator[Component]:
for item in components:
if isinstance(item, ActionRowComponent):
yield from item.children
elif isinstance(item, ContainerComponent):
yield from _walk_all_components(item.children)
elif isinstance(item, SectionComponent):
yield from item.children
yield item.accessory
else:
yield item
def _component_to_item(component: Component) -> Item:
if isinstance(component, ButtonComponent):
def _component_to_item(component: Component, parent: Optional[Item] = None) -> Item:
if isinstance(component, ActionRowComponent):
from .action_row import ActionRow
item = ActionRow.from_component(component)
elif isinstance(component, ButtonComponent):
from .button import Button
return Button.from_component(component)
if isinstance(component, SelectComponent):
item = Button.from_component(component)
elif isinstance(component, SelectComponent):
from .select import BaseSelect
return BaseSelect.from_component(component)
item = BaseSelect.from_component(component)
elif isinstance(component, SectionComponent):
from .section import Section
item = Section.from_component(component)
elif isinstance(component, TextDisplayComponent):
from .text_display import TextDisplay
item = TextDisplay.from_component(component)
elif isinstance(component, MediaGalleryComponent):
from .media_gallery import MediaGallery
return Item.from_component(component)
item = MediaGallery.from_component(component)
elif isinstance(component, FileComponent):
from .file import File
item = File.from_component(component)
elif isinstance(component, SeparatorComponent):
from .separator import Separator
item = Separator.from_component(component)
elif isinstance(component, ThumbnailComponent):
from .thumbnail import Thumbnail
item = Thumbnail.from_component(component)
elif isinstance(component, ContainerComponent):
from .container import Container
item = Container.from_component(component)
elif isinstance(component, LabelComponent):
from .label import Label
item = Label.from_component(component)
else:
item = Item.from_component(component)
item._parent = parent
return item
class _ViewWeights:
@ -133,73 +207,66 @@ class _ViewWeights:
class _ViewCallback:
__slots__ = ('view', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[Any, Any], view: View, item: Item[View]) -> None:
def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.view: View = view
self.item: Item[View] = item
self.view: BaseView = view
self.item: Item[BaseView] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.view, interaction, self.item)
class View:
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
__discord_ui_view__: ClassVar[bool] = True
class BaseView:
__discord_ui_view__: ClassVar[bool] = False
__discord_ui_modal__: ClassVar[bool] = False
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
__view_children_items__: ClassVar[Dict[str, ItemLike]] = {}
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = list(children.values())
def _init_children(self) -> List[Item[Self]]:
children = []
for func in self.__view_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item) # type: ignore
item._view = self
if isinstance(item, Select):
item.options = [option.copy() for option in item.options]
setattr(self, func.__name__, item)
children.append(item)
return children
def __init__(self, *, timeout: Optional[float] = 180.0):
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
self.__timeout = timeout
self._children: List[Item[Self]] = self._init_children()
self.__weights = _ViewWeights(self._children)
self.id: str = os.urandom(16).hex()
self._cache_key: Optional[int] = None
self.__cancel_callback: Optional[Callable[[View], None]] = None
self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
self._total_children: int = len(tuple(self.walk_children()))
def _is_layout(self) -> bool:
return False
def __repr__(self) -> str:
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>'
def _init_children(self) -> List[Item[Self]]:
children = []
parents = {}
for name, raw in self.__view_children_items__.items():
if isinstance(raw, Item):
item = raw.copy()
setattr(self, name, item)
item._update_view(self)
parent = getattr(item, '__discord_ui_parent__', None)
if parent and parent._view is None:
parent._view = self
children.append(item)
parents[raw] = item
else:
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(raw, self, item) # type: ignore
item._view = self
if isinstance(item, Select):
item.options = [option.copy() for option in item.options]
setattr(self, raw.__name__, item)
parent = getattr(raw, '__discord_ui_parent__', None)
if parent:
parents.get(parent, parent)._children.append(item)
continue
children.append(item)
return children
async def __timeout_task_impl(self) -> None:
while True:
# Guard just in case someone changes the value of the timeout at runtime
@ -218,29 +285,16 @@ class View:
await asyncio.sleep(self.__timeout_expiry - now)
def is_dispatchable(self) -> bool:
# this is used by webhooks to check whether a view requires a state attached
# or not, this simply is, whether a view has a component other than a url button
return any(item.is_dispatchable() for item in self.children)
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
# checks whether any interactable items (buttons or selects) are present
# in this view, and check whether this requires a state attached in case
# of webhooks and if the view should be stored in the view store
return any(item.is_dispatchable() for item in self.walk_children())
components.append(
{
'type': 1,
'components': children,
}
)
def has_components_v2(self) -> bool:
return any(c._is_v2() for c in self.children)
return components
def to_components(self) -> List[Dict[str, Any]]:
return NotImplemented
def _refresh_timeout(self) -> None:
if self.__timeout:
@ -265,19 +319,32 @@ class View:
self.__timeout = value
def _add_count(self, value: int) -> None:
self._total_children = max(0, self._total_children + value)
@property
def children(self) -> List[Item[Self]]:
"""List[:class:`Item`]: The list of children attached to this view."""
return self._children.copy()
@property
def total_children_count(self) -> int:
""":class:`int`: The total number of children in this view, including those from nested items."""
return self._total_children
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
"""Converts a message's components into a :class:`View`.
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Union[View, LayoutView]:
"""Converts a message's components into a :class:`View`
or :class:`LayoutView`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``discord.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`View` first.
converted into a :class:`View` or :class:`LayoutView` first.
If the message has any v2 components, then you must use
:class:`LayoutView` in order for them to be converted into
their respective items. :class:`View` does not support v2 components.
Parameters
-----------
@ -287,24 +354,43 @@ class View:
The timeout of the converted view.
Returns
--------
:class:`View`
The converted view. This always returns a :class:`View` and not
one of its subclasses.
-------
Union[:class:`View`, :class:`LayoutView`]
The converted view. This will always return one of :class:`View` or
:class:`LayoutView`, and not one of its subclasses.
"""
view = View(timeout=timeout)
if issubclass(cls, View):
view_cls = View
elif issubclass(cls, LayoutView):
view_cls = LayoutView
else:
raise TypeError('unreachable exception')
view = view_cls(timeout=timeout)
row = 0
for component in message.components:
if isinstance(component, ActionRowComponent):
if not view._is_layout() and isinstance(component, ActionRowComponent):
for child in component.children:
item = _component_to_item(child)
item.row = row
# this error should never be raised, because ActionRows can only
# contain items that View accepts, but check anyways
if item._is_v2():
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
view.add_item(item)
row += 1
else:
item = _component_to_item(component)
item.row = row
view.add_item(item)
continue
item = _component_to_item(component)
item.row = row
if item._is_v2() and not view._is_layout():
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
view.add_item(item)
row += 1
return view
@ -324,19 +410,24 @@ class View:
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (25)
or the row the item is trying to be added to is full.
Maximum number of children has been exceeded, the
row the item is trying to be added to is full or the item
you tried to add is not allowed in this View.
"""
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
self.__weights.add_item(item)
if item._is_v2() and not self._is_layout():
raise ValueError('v2 items cannot be added to this view')
item._view = self
item._update_view(self)
added = 1
if item._has_children():
added += len(tuple(item.walk_children())) # type: ignore
self._add_count(added)
self._children.append(item)
return self
@ -357,7 +448,11 @@ class View:
except ValueError:
pass
else:
self.__weights.remove_item(item)
removed = 1
if item._has_children():
removed += len(tuple(item.walk_children())) # type: ignore
self._add_count(-removed)
return self
def clear_items(self) -> Self:
@ -367,9 +462,31 @@ class View:
chaining.
"""
self._children.clear()
self.__weights.clear()
self._total_children = 0
return self
def find_item(self, id: int, /) -> Optional[Item[Self]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
.. versionadded:: 2.6
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
async def interaction_check(self, interaction: Interaction, /) -> bool:
"""|coro|
@ -428,7 +545,7 @@ class View:
try:
item._refresh_state(interaction, interaction.data) # type: ignore
allow = await item.interaction_check(interaction) and await self.interaction_check(interaction)
allow = await item._run_checks(interaction) and await self.interaction_check(interaction)
if not allow:
return
@ -440,7 +557,7 @@ class View:
return await self.on_error(interaction, e, item)
def _start_listening_from_store(self, store: ViewStore) -> None:
self.__cancel_callback = partial(store.remove_view)
self.__cancel_callback = partial(store.remove_view) # type: ignore
if self.timeout:
if self.__timeout_task is not None:
self.__timeout_task.cancel()
@ -459,17 +576,17 @@ class View:
self.__stopped.set_result(True)
asyncio.create_task(self.on_timeout(), name=f'discord-ui-view-timeout-{self.id}')
def _dispatch_item(self, item: Item, interaction: Interaction):
def _dispatch_item(self, item: Item, interaction: Interaction) -> Optional[asyncio.Task[None]]:
if self.__stopped.done():
return
asyncio.create_task(self._scheduled_task(item, interaction), name=f'discord-ui-view-dispatch-{self.id}')
return asyncio.create_task(self._scheduled_task(item, interaction), name=f'discord-ui-view-dispatch-{self.id}')
def _refresh(self, components: List[Component]) -> None:
# fmt: off
old_state: Dict[str, Item[Any]] = {
item.custom_id: item # type: ignore
for item in self._children
for item in self.walk_children()
if item.is_dispatchable()
}
# fmt: on
@ -536,21 +653,210 @@ class View:
"""
return await self.__stopped
def walk_children(self) -> Generator[Item[Any], None, None]:
"""An iterator that recursively walks through all the children of this view
and its children, if applicable.
Yields
------
:class:`Item`
An item in the view.
"""
for child in self.children:
yield child
if child._has_children():
yield from child.walk_children() # type: ignore
class View(BaseView):
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
__discord_ui_view__: ClassVar[bool] = True
if TYPE_CHECKING:
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: ...
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemLike] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
elif isinstance(member, Item) and member._is_v2():
raise ValueError(f'{name} cannot be added to this View')
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = children
def __init__(self, *, timeout: Optional[float] = 180.0):
super().__init__(timeout=timeout)
self.__weights = _ViewWeights(self._children)
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
'type': 1,
'components': children,
}
)
return components
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
super().add_item(item)
try:
self.__weights.add_item(item)
except ValueError as e:
# if the item has no space left then remove it from _children
self._children.remove(item)
raise e
return self
def remove_item(self, item: Item[Any]) -> Self:
try:
self._children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
return self
def clear_items(self) -> Self:
super().clear_items()
self.__weights.clear()
return self
class LayoutView(BaseView):
"""Represents a layout view for components.
This object must be inherited to create a UI within Discord.
This differs from a :class:`View` in that it supports all component types
and uses what Discord refers to as "v2 components".
You can find usage examples in the :resource:`repository <examples>`
.. versionadded:: 2.6
Parameters
----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
if TYPE_CHECKING:
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView: ...
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
super().__init__(timeout=timeout)
if self._total_children > 40:
raise ValueError('maximum number of children exceeded (40)')
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemLike] = {}
callback_children: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if isinstance(member, Item):
if member._parent is not None:
continue
member._rendered_row = member._row
children[name] = member
elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
callback_children[name] = member
children.update(callback_children)
cls.__view_children_items__ = children
def _is_layout(self) -> bool:
return True
def _add_count(self, value: int) -> None:
if self._total_children + value > 40:
raise ValueError('maximum number of children exceeded (40)')
self._total_children = max(0, self._total_children + value)
def to_components(self):
components: List[Dict[str, Any]] = []
for i in self._children:
components.append(i.to_component_dict())
return components
def add_item(self, item: Item[Any]) -> Self:
if self._total_children >= 40:
raise ValueError('maximum number of children exceeded (40)')
super().add_item(item)
return self
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in the view's items.
A view is allowed to have a maximum of 4000 display characters across all its items.
"""
from .text_display import TextDisplay
return sum(len(item.content) for item in self.walk_children() if isinstance(item, TextDisplay))
class ViewStore:
def __init__(self, state: ConnectionState):
# entity_id: {(component_type, custom_id): Item}
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {}
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {}
# message_id: View
self._synced_message_views: Dict[int, View] = {}
self._synced_message_views: Dict[int, BaseView] = {}
# custom_id: Modal
self._modals: Dict[str, Modal] = {}
# component_type is the key
self._dynamic_items: Dict[re.Pattern[str], Type[DynamicItem[Item[Any]]]] = {}
self._state: ConnectionState = state
self.__tasks: Set[asyncio.Task[None]] = set()
@property
def persistent_views(self) -> Sequence[View]:
def persistent_views(self) -> Sequence[BaseView]:
# fmt: off
views = {
item.view.id: item.view
@ -561,6 +867,10 @@ class ViewStore:
# fmt: on
return list(views.values())
def add_task(self, task: asyncio.Task[None]) -> None:
self.__tasks.add(task)
task.add_done_callback(self.__tasks.discard)
def add_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
for item in items:
pattern = item.__discord_ui_compiled_template__
@ -571,7 +881,7 @@ class ViewStore:
pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None)
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None:
view._start_listening_from_store(self)
if view.__discord_ui_modal__:
self._modals[view.custom_id] = view # type: ignore
@ -579,7 +889,7 @@ class ViewStore:
dispatch_info = self._views.setdefault(message_id, {})
is_fully_dynamic = True
for item in view._children:
for item in view.walk_children():
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items[pattern] = item.__class__
@ -621,15 +931,16 @@ class ViewStore:
if interaction.message is None:
return
view = View.from_message(interaction.message, timeout=None)
view_cls = View if not interaction.message.flags.components_v2 else LayoutView
view = view_cls.from_message(interaction.message, timeout=None)
try:
base_item_index, base_item = next(
(index, child)
for index, child in enumerate(view._children)
if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id
)
except StopIteration:
base_item = _utils_find(
lambda i: i.type.value == component_type and getattr(i, 'custom_id', None) == custom_id,
view.walk_children(),
)
# if the item is not found then return
if not base_item:
return
try:
@ -638,8 +949,17 @@ class ViewStore:
_log.exception('Ignoring exception in dynamic item creation for %r', factory)
return
# Swap the item in the view with our new dynamic item
view._children[base_item_index] = item
# Swap the item in the view or parent with our new dynamic item
# Prioritize the item parent:
parent = base_item._parent or view
try:
child_index = parent._children.index(base_item) # type: ignore
except ValueError:
return
else:
parent._children[child_index] = item # type: ignore
item._view = view
item._rendered_row = base_item._rendered_row
item._refresh_state(interaction, interaction.data) # type: ignore
@ -661,9 +981,11 @@ class ViewStore:
for pattern, item in self._dynamic_items.items():
match = pattern.fullmatch(custom_id)
if match is not None:
asyncio.create_task(
self.schedule_dynamic_item_call(component_type, item, interaction, custom_id, match),
name=f'discord-ui-dynamic-item-{item.__name__}-{custom_id}',
self.add_task(
asyncio.create_task(
self.schedule_dynamic_item_call(component_type, item, interaction, custom_id, match),
name=f'discord-ui-dynamic-item-{item.__name__}-{custom_id}',
)
)
def dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
@ -681,7 +1003,7 @@ class ViewStore:
key = (component_type, custom_id)
# The entity_id can either be message_id, interaction_id, or None in that priority order.
item: Optional[Item[View]] = None
item: Optional[Item[BaseView]] = None
if message_id is not None:
item = self._views.get(message_id, {}).get(key)
@ -710,7 +1032,9 @@ class ViewStore:
return
# Note, at this point the View is *not* None
item.view._dispatch_item(item, interaction) # type: ignore
task = item.view._dispatch_item(item, interaction) # type: ignore
if task is not None:
self.add_task(task)
def dispatch_modal(
self,
@ -720,10 +1044,10 @@ class ViewStore:
) -> None:
modal = self._modals.get(custom_id)
if modal is None:
_log.debug("Modal interaction referencing unknown custom_id %s. Discarding", custom_id)
_log.debug('Modal interaction referencing unknown custom_id %s. Discarding', custom_id)
return
modal._dispatch_submit(interaction, components)
self.add_task(modal._dispatch_submit(interaction, components))
def remove_interaction_mapping(self, interaction_id: int) -> None:
# This is called before re-adding the view
@ -733,14 +1057,14 @@ class ViewStore:
def is_message_tracked(self, message_id: int) -> bool:
return message_id in self._synced_message_views
def remove_message_tracking(self, message_id: int) -> Optional[View]:
def remove_message_tracking(self, message_id: int) -> Optional[BaseView]:
return self._synced_message_views.pop(message_id, None)
def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> None:
def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None:
components: List[Component] = []
for component_data in data:
component = _component_factory(component_data)
component = _component_factory(component_data, self._state) # type: ignore
if component is not None:
components.append(component)

4
discord/user.py

@ -101,8 +101,8 @@ class BaseUser(_UserTag):
def __repr__(self) -> str:
return (
f"<BaseUser id={self.id} name={self.name!r} global_name={self.global_name!r}"
f" bot={self.bot} system={self.system}>"
f'<BaseUser id={self.id} name={self.name!r} global_name={self.global_name!r}'
f' bot={self.bot} system={self.system}>'
)
def __str__(self) -> str:

84
discord/utils.py

@ -158,8 +158,7 @@ if TYPE_CHECKING:
class _DecompressionContext(Protocol):
COMPRESSION_TYPE: str
def decompress(self, data: bytes, /) -> str | None:
...
def decompress(self, data: bytes, /) -> str | None: ...
P = ParamSpec('P')
@ -186,12 +185,10 @@ class CachedSlotProperty(Generic[T, T_co]):
self.__doc__ = getattr(function, '__doc__')
@overload
def __get__(self, instance: None, owner: Type[T]) -> CachedSlotProperty[T, T_co]:
...
def __get__(self, instance: None, owner: Type[T]) -> CachedSlotProperty[T, T_co]: ...
@overload
def __get__(self, instance: T, owner: Type[T]) -> T_co:
...
def __get__(self, instance: T, owner: Type[T]) -> T_co: ...
def __get__(self, instance: Optional[T], owner: Type[T]) -> Any:
if instance is None:
@ -240,15 +237,13 @@ class SequenceProxy(Sequence[T_co]):
return self.__proxied
def __repr__(self) -> str:
return f"SequenceProxy({self.__proxied!r})"
return f'SequenceProxy({self.__proxied!r})'
@overload
def __getitem__(self, idx: SupportsIndex) -> T_co:
...
def __getitem__(self, idx: SupportsIndex) -> T_co: ...
@overload
def __getitem__(self, idx: slice) -> List[T_co]:
...
def __getitem__(self, idx: slice) -> List[T_co]: ...
def __getitem__(self, idx: Union[SupportsIndex, slice]) -> Union[T_co, List[T_co]]:
return self.__copied[idx]
@ -273,18 +268,15 @@ class SequenceProxy(Sequence[T_co]):
@overload
def parse_time(timestamp: None) -> None:
...
def parse_time(timestamp: None) -> None: ...
@overload
def parse_time(timestamp: str) -> datetime.datetime:
...
def parse_time(timestamp: str) -> datetime.datetime: ...
@overload
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
...
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]: ...
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
@ -308,7 +300,7 @@ def deprecated(instead: Optional[str] = None) -> Callable[[Callable[P, T]], Call
def decorated(*args: P.args, **kwargs: P.kwargs) -> T:
warnings.simplefilter('always', DeprecationWarning) # turn off filter
if instead:
fmt = "{0.__name__} is deprecated, use {1} instead."
fmt = '{0.__name__} is deprecated, use {1} instead.'
else:
fmt = '{0.__name__} is deprecated.'
@ -447,13 +439,11 @@ async def _afind(predicate: Callable[[T], Any], iterable: AsyncIterable[T], /) -
@overload
def find(predicate: Callable[[T], Any], iterable: AsyncIterable[T], /) -> Coro[Optional[T]]:
...
def find(predicate: Callable[[T], Any], iterable: AsyncIterable[T], /) -> Coro[Optional[T]]: ...
@overload
def find(predicate: Callable[[T], Any], iterable: Iterable[T], /) -> Optional[T]:
...
def find(predicate: Callable[[T], Any], iterable: Iterable[T], /) -> Optional[T]: ...
def find(predicate: Callable[[T], Any], iterable: _Iter[T], /) -> Union[Optional[T], Coro[Optional[T]]]:
@ -533,13 +523,11 @@ async def _aget(iterable: AsyncIterable[T], /, **attrs: Any) -> Optional[T]:
@overload
def get(iterable: AsyncIterable[T], /, **attrs: Any) -> Coro[Optional[T]]:
...
def get(iterable: AsyncIterable[T], /, **attrs: Any) -> Coro[Optional[T]]: ...
@overload
def get(iterable: Iterable[T], /, **attrs: Any) -> Optional[T]:
...
def get(iterable: Iterable[T], /, **attrs: Any) -> Optional[T]: ...
def get(iterable: _Iter[T], /, **attrs: Any) -> Union[Optional[T], Coro[Optional[T]]]:
@ -622,7 +610,7 @@ def _get_as_snowflake(data: Any, key: str) -> Optional[int]:
def _get_mime_type_for_image(data: bytes):
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
if data.startswith(b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a'):
return 'image/png'
elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'):
return 'image/jpeg'
@ -756,13 +744,11 @@ def compute_timedelta(dt: datetime.datetime) -> float:
@overload
async def sleep_until(when: datetime.datetime, result: T) -> T:
...
async def sleep_until(when: datetime.datetime, result: T) -> T: ...
@overload
async def sleep_until(when: datetime.datetime) -> None:
...
async def sleep_until(when: datetime.datetime) -> None: ...
async def sleep_until(when: datetime.datetime, result: Optional[T] = None) -> Optional[T]:
@ -823,8 +809,7 @@ class SnowflakeList(_SnowflakeListBase):
if TYPE_CHECKING:
def __init__(self, data: Iterable[int], *, is_sorted: bool = False):
...
def __init__(self, data: Iterable[int], *, is_sorted: bool = False): ...
def __new__(cls, data: Iterable[int], *, is_sorted: bool = False) -> Self:
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data)) # type: ignore
@ -934,11 +919,11 @@ _MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)|^#{1,3}|^\s*-'
_MARKDOWN_ESCAPE_REGEX = re.compile(fr'(?P<markdown>{_MARKDOWN_ESCAPE_SUBREGEX}|{_MARKDOWN_ESCAPE_COMMON})', re.MULTILINE)
_MARKDOWN_ESCAPE_REGEX = re.compile(rf'(?P<markdown>{_MARKDOWN_ESCAPE_SUBREGEX}|{_MARKDOWN_ESCAPE_COMMON})', re.MULTILINE)
_URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\'\]\s])'
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
_MARKDOWN_STOCK_REGEX = rf'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
def remove_markdown(text: str, *, ignore_links: bool = True) -> str:
@ -1073,13 +1058,11 @@ async def _achunk(iterator: AsyncIterable[T], max_size: int) -> AsyncIterator[Li
@overload
def as_chunks(iterator: AsyncIterable[T], max_size: int) -> AsyncIterator[List[T]]:
...
def as_chunks(iterator: AsyncIterable[T], max_size: int) -> AsyncIterator[List[T]]: ...
@overload
def as_chunks(iterator: Iterable[T], max_size: int) -> Iterator[List[T]]:
...
def as_chunks(iterator: Iterable[T], max_size: int) -> Iterator[List[T]]: ...
def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]:
@ -1304,7 +1287,6 @@ def stream_supports_colour(stream: Any) -> bool:
class _ColourFormatter(logging.Formatter):
# ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
# It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
# The important ones here relate to colour.
@ -1499,37 +1481,37 @@ def _format_call_duration(duration: datetime.timedelta) -> str:
threshold_M = 10.5
if seconds < threshold_s:
formatted = "a few seconds"
formatted = 'a few seconds'
elif seconds < (threshold_m * minutes_s):
minutes = round(seconds / minutes_s)
if minutes == 1:
formatted = "a minute"
formatted = 'a minute'
else:
formatted = f"{minutes} minutes"
formatted = f'{minutes} minutes'
elif seconds < (threshold_h * hours_s):
hours = round(seconds / hours_s)
if hours == 1:
formatted = "an hour"
formatted = 'an hour'
else:
formatted = f"{hours} hours"
formatted = f'{hours} hours'
elif seconds < (threshold_d * days_s):
days = round(seconds / days_s)
if days == 1:
formatted = "a day"
formatted = 'a day'
else:
formatted = f"{days} days"
formatted = f'{days} days'
elif seconds < (threshold_M * months_s):
months = round(seconds / months_s)
if months == 1:
formatted = "a month"
formatted = 'a month'
else:
formatted = f"{months} months"
formatted = f'{months} months'
else:
years = round(seconds / years_s)
if years == 1:
formatted = "a year"
formatted = 'a year'
else:
formatted = f"{years} years"
formatted = f'{years} years'
return formatted

6
discord/voice_client.py

@ -217,7 +217,7 @@ class VoiceClient(VoiceProtocol):
def __init__(self, client: Client, channel: abc.Connectable) -> None:
if not has_nacl:
raise RuntimeError("PyNaCl library needed in order to use voice")
raise RuntimeError('PyNaCl library needed in order to use voice')
super().__init__(client, channel)
state = client._connection
@ -321,7 +321,7 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4
"""
ws = self._connection.ws
return float("inf") if not ws else ws.latency
return float('inf') if not ws else ws.latency
@property
def average_latency(self) -> float:
@ -330,7 +330,7 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4
"""
ws = self._connection.ws
return float("inf") if not ws else ws.average_latency
return float('inf') if not ws else ws.average_latency
async def disconnect(self, *, force: bool = False) -> None:
"""|coro|

2
discord/voice_state.py

@ -148,7 +148,7 @@ class SocketReader(threading.Thread):
readable, _, _ = select.select([self.state.socket], [], [], 30)
except (ValueError, TypeError, OSError) as e:
_log.debug(
"Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e
'Select error handling socket in reader, this should be safe to ignore: %s: %s', e.__class__.__name__, e
)
# The socket is either closed or doesn't exist at the moment
continue

102
discord/webhook/async_.py

@ -71,7 +71,7 @@ if TYPE_CHECKING:
from ..emoji import Emoji
from ..channel import VoiceChannel
from ..abc import Snowflake
from ..ui.view import View
from ..ui.view import BaseView, View, LayoutView
from ..poll import Poll
import datetime
from ..types.webhook import (
@ -552,7 +552,7 @@ def interaction_message_response_params(
embed: Optional[Embed] = MISSING,
embeds: Sequence[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[BaseView] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
previous_allowed_mentions: Optional[AllowedMentions] = None,
poll: Poll = MISSING,
@ -592,6 +592,13 @@ def interaction_message_response_params(
if view is not MISSING:
if view is not None:
data['components'] = view.to_components()
if view.has_components_v2():
if flags is not MISSING:
flags.components_v2 = True
else:
flags = MessageFlags(components_v2=True)
else:
data['components'] = []
@ -755,7 +762,7 @@ class _WebhookState:
def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[PartialEmoji, Emoji, str]:
if self._parent is not None:
return self._parent.get_reaction_emoji(data)
return self._parent.get_emoji_from_partial_payload(data)
emoji_id = utils._get_as_snowflake(data, 'id')
@ -802,7 +809,7 @@ class WebhookMessage(Message):
embeds: Sequence[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[BaseView] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> WebhookMessage:
"""|coro|
@ -1317,7 +1324,7 @@ class Webhook(BaseWebhook):
@classmethod
def _as_follower(cls, data, *, channel, user) -> Self:
name = f"{channel.guild} #{channel}"
name = f'{channel.guild} #{channel}'
feed: WebhookPayload = {
'id': data['webhook_id'],
'type': 2,
@ -1598,6 +1605,44 @@ class Webhook(BaseWebhook):
# state is artificial
return WebhookMessage(data=data, state=state, channel=channel) # type: ignore
@overload
async def send(
self,
*,
username: str = MISSING,
avatar_url: Any = MISSING,
ephemeral: bool = MISSING,
file: File = MISSING,
files: Sequence[File] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: LayoutView,
wait: Literal[True],
thread: Snowflake = MISSING,
thread_name: str = MISSING,
suppress_embeds: bool = MISSING,
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
) -> WebhookMessage: ...
@overload
async def send(
self,
*,
username: str = MISSING,
avatar_url: Any = MISSING,
ephemeral: bool = MISSING,
file: File = MISSING,
files: Sequence[File] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: LayoutView,
wait: Literal[False] = ...,
thread: Snowflake = MISSING,
thread_name: str = MISSING,
suppress_embeds: bool = MISSING,
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
) -> None: ...
@overload
async def send(
self,
@ -1620,8 +1665,7 @@ class Webhook(BaseWebhook):
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
) -> WebhookMessage:
...
) -> WebhookMessage: ...
@overload
async def send(
@ -1645,8 +1689,7 @@ class Webhook(BaseWebhook):
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
) -> None:
...
) -> None: ...
async def send(
self,
@ -1661,7 +1704,7 @@ class Webhook(BaseWebhook):
embed: Embed = MISSING,
embeds: Sequence[Embed] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: View = MISSING,
view: BaseView = MISSING,
thread: Snowflake = MISSING,
thread_name: str = MISSING,
wait: bool = False,
@ -1727,7 +1770,7 @@ class Webhook(BaseWebhook):
Controls the mentions being processed in this message.
.. versionadded:: 1.4
view: :class:`discord.ui.View`
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
The view to send with the message. If the webhook is partial or
is not managed by the library, then you can only send URL buttons.
Otherwise, you can send views with any type of components.
@ -1931,6 +1974,31 @@ class Webhook(BaseWebhook):
)
return self._create_message(data, thread=thread)
@overload
async def edit_message(
self,
message_id: int,
*,
attachments: Sequence[Union[Attachment, File]] = ...,
view: LayoutView,
allowed_mentions: Optional[AllowedMentions] = ...,
thread: Snowflake = ...,
) -> WebhookMessage: ...
@overload
async def edit_message(
self,
message_id: int,
*,
content: Optional[str] = ...,
embeds: Sequence[Embed] = ...,
embed: Optional[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
view: Optional[View] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
thread: Snowflake = ...,
) -> WebhookMessage: ...
async def edit_message(
self,
message_id: int,
@ -1939,7 +2007,7 @@ class Webhook(BaseWebhook):
embeds: Sequence[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
view: Optional[BaseView] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
thread: Snowflake = MISSING,
) -> WebhookMessage:
@ -1978,11 +2046,17 @@ class Webhook(BaseWebhook):
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
view: Optional[:class:`~discord.ui.View`]
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. If ``None`` is passed then
the view is removed. The webhook must have state attached, similar to
:meth:`send`.
.. note::
To update the message to add a :class:`~discord.ui.LayoutView`, you
must explicitly set the ``content``, ``embed``, ``embeds``, and
``attachments`` parameters to either ``None`` or an empty array, as appropriate.
.. versionadded:: 2.0
thread: :class:`~discord.abc.Snowflake`
The thread the webhook message belongs to.
@ -2046,7 +2120,7 @@ class Webhook(BaseWebhook):
)
message = self._create_message(data, thread=thread)
if view and not view.is_finished():
if view and not view.is_finished() and view.is_dispatchable():
self._state.store_view(view, message_id)
return message

85
discord/webhook/sync.py

@ -66,7 +66,7 @@ if TYPE_CHECKING:
from ..message import Attachment
from ..abc import Snowflake
from ..state import ConnectionState
from ..ui import View
from ..ui.view import BaseView, View, LayoutView
from ..types.webhook import (
Webhook as WebhookPayload,
)
@ -856,6 +856,42 @@ class SyncWebhook(BaseWebhook):
# state is artificial
return SyncWebhookMessage(data=data, state=state, channel=channel) # type: ignore
@overload
def send(
self,
*,
username: str = MISSING,
avatar_url: Any = MISSING,
file: File = MISSING,
files: Sequence[File] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: LayoutView,
wait: Literal[True],
thread: Snowflake = MISSING,
thread_name: str = MISSING,
suppress_embeds: bool = MISSING,
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
) -> SyncWebhookMessage: ...
@overload
def send(
self,
*,
username: str = MISSING,
avatar_url: Any = MISSING,
file: File = MISSING,
files: Sequence[File] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: LayoutView,
wait: Literal[False] = ...,
thread: Snowflake = MISSING,
thread_name: str = MISSING,
suppress_embeds: bool = MISSING,
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
) -> None: ...
@overload
def send(
self,
@ -876,8 +912,8 @@ class SyncWebhook(BaseWebhook):
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
) -> SyncWebhookMessage:
...
view: View = MISSING,
) -> SyncWebhookMessage: ...
@overload
def send(
@ -899,8 +935,8 @@ class SyncWebhook(BaseWebhook):
silent: bool = MISSING,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
) -> None:
...
view: View = MISSING,
) -> None: ...
def send(
self,
@ -921,7 +957,7 @@ class SyncWebhook(BaseWebhook):
silent: bool = False,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
view: View = MISSING,
view: BaseView = MISSING,
) -> Optional[SyncWebhookMessage]:
"""Sends a message using the webhook.
@ -994,8 +1030,8 @@ class SyncWebhook(BaseWebhook):
When sending a Poll via webhook, you cannot manually end it.
.. versionadded:: 2.4
view: :class:`~discord.ui.View`
The view to send with the message. This can only have URL buttons, which donnot
view: Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]
The view to send with the message. This can only have non-interactible items, which do not
require a state to be attached to it.
If you want to send a view with any component attached to it, check :meth:`Webhook.send`.
@ -1143,6 +1179,31 @@ class SyncWebhook(BaseWebhook):
)
return self._create_message(data, thread=thread)
@overload
def edit_message(
self,
message_id: int,
*,
attachments: Sequence[Union[Attachment, File]] = ...,
view: LayoutView,
allowed_mentions: Optional[AllowedMentions] = ...,
thread: Snowflake = ...,
) -> SyncWebhookMessage: ...
@overload
def edit_message(
self,
message_id: int,
*,
content: Optional[str] = ...,
embeds: Sequence[Embed] = ...,
embed: Optional[Embed] = ...,
attachments: Sequence[Union[Attachment, File]] = ...,
view: Optional[View] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
thread: Snowflake = ...,
) -> SyncWebhookMessage: ...
def edit_message(
self,
message_id: int,
@ -1151,6 +1212,7 @@ class SyncWebhook(BaseWebhook):
embeds: Sequence[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[BaseView] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
thread: Snowflake = MISSING,
) -> SyncWebhookMessage:
@ -1177,6 +1239,13 @@ class SyncWebhook(BaseWebhook):
then all attachments are removed.
.. versionadded:: 2.0
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
The updated view to update this message with. This can only have non-interactible items, which do not
require a state to be attached to it. If ``None`` is passed then the view is removed.
If you want to edit a webhook message with any component attached to it, check :meth:`WebhookMessage.edit`.
.. versionadded:: 2.6
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.

4
discord/widget.py

@ -195,7 +195,7 @@ class WidgetMember(BaseUser):
self.connected_channel: Optional[WidgetChannel] = connected_channel
def __repr__(self) -> str:
return f"<WidgetMember name={self.name!r} global_name={self.global_name!r}" f" bot={self.bot} nick={self.nick!r}>"
return f'<WidgetMember name={self.name!r} global_name={self.global_name!r} bot={self.bot} nick={self.nick!r}>'
@property
def display_name(self) -> str:
@ -292,7 +292,7 @@ class Widget:
@property
def json_url(self) -> str:
""":class:`str`: The JSON URL of the widget."""
return f"https://discord.com/api/guilds/{self.id}/widget.json"
return f'https://discord.com/api/guilds/{self.id}/widget.json'
@property
def invite_url(self) -> Optional[str]:

305
docs/api.rst

@ -3120,6 +3120,104 @@ of :class:`enum.Enum`.
.. versionadded:: 2.5
.. attribute:: onboarding_prompt_create
A guild onboarding prompt was created.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
a :class:`Object` with the ID of the prompt that the options belong to.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.type`
- :attr:`~AuditLogDiff.title`
- :attr:`~AuditLogDiff.options`
- :attr:`~AuditLogDiff.single_select`
- :attr:`~AuditLogDiff.required`
- :attr:`~AuditLogDiff.in_onboarding`
.. versionadded:: 2.6
.. attribute:: onboarding_prompt_update
A guild onboarding prompt was updated.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
a :class:`Object` with the ID of the prompt that the options belong to.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.type`
- :attr:`~AuditLogDiff.title`
- :attr:`~AuditLogDiff.options`
- :attr:`~AuditLogDiff.single_select`
- :attr:`~AuditLogDiff.required`
- :attr:`~AuditLogDiff.in_onboarding`
.. versionadded:: 2.6
.. attribute:: onboarding_prompt_delete
A guild onboarding prompt was deleted.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
a :class:`Object` with the ID of the prompt that the options belong to.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.type`
- :attr:`~AuditLogDiff.title`
- :attr:`~AuditLogDiff.options`
- :attr:`~AuditLogDiff.single_select`
- :attr:`~AuditLogDiff.required`
- :attr:`~AuditLogDiff.in_onboarding`
.. versionadded:: 2.6
.. attribute:: onboarding_create
The guild's onboarding configuration was created.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
always ``None``. Use :attr:`~AuditLogEntry.guild` to access the guild.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.enabled`
- :attr:`~AuditLogDiff.default_channels`
- :attr:`~AuditLogDiff.prompts`
- :attr:`~AuditLogDiff.mode`
.. versionadded:: 2.6
.. attribute:: onboarding_update
The guild's onboarding configuration was updated.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
always ``None``. Use :attr:`~AuditLogEntry.guild` to access the guild.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.enabled`
- :attr:`~AuditLogDiff.default_channels`
- :attr:`~AuditLogDiff.prompts`
- :attr:`~AuditLogDiff.mode`
.. versionadded:: 2.6
.. attribute:: home_settings_create
The guild's server guide was created.
.. versionadded:: 2.6
.. attribute:: home_settings_update
The guild's server guide was updated.
.. versionadded:: 2.6
.. class:: AuditLogActionCategory
Represents the category that the :class:`AuditLogAction` belongs to.
@ -3506,6 +3604,15 @@ of :class:`enum.Enum`.
The ``vi`` locale.
.. attribute:: language_code
:class:`str`: Returns the locale's BCP 47 language code in the format of ``language-COUNTRY``.
This is derived from a predefined mapping based on Discord's supported locales.
If no mapping exists for the current locale, this returns the raw locale value as a fallback.
.. versionadded:: 2.6
.. class:: MFALevel
@ -3898,6 +4005,75 @@ of :class:`enum.Enum`.
An alias for :attr:`.default`.
.. class:: StatusDisplayType
Represents which field is of the user's activity is
displayed in the members list.
.. versionadded:: 2.6
.. attribute:: name
The name of the activity is displayed.
.. attribute:: state
The state of the activity is displayed.
.. attribute:: details
The details of the activity are displayed.
.. class:: OnboardingPromptType
Represents the type of onboarding prompt.
.. versionadded:: 2.6
.. attribute:: multiple_choice
Prompt options are multiple choice.
.. attribute:: dropdown
Prompt options are displayed as a drop-down.
.. class:: OnboardingMode
Represents the onboarding constraint mode.
.. versionadded:: 2.6
.. attribute:: default
Only default channels count towards onboarding constraints.
.. attribute:: advanced
Default channels and questions count towards onboarding constraints.
.. class:: MediaItemLoadingState
Represents a :class:`UnfurledMediaItem` load state.
.. attribute:: unknown
Unknown load state.
.. attribute:: loading
The media item is still loading.
.. attribute:: loaded
The media item is loaded.
.. attribute:: not_found
The media item was not found.
.. _discord-api-audit-logs:
Audit Log Data
@ -4144,9 +4320,9 @@ AuditLogDiff
.. attribute:: type
The type of channel, sticker, webhook or integration.
The type of channel, sticker, webhook, integration or onboarding prompt.
:type: Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`]
:type: Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`, :class:`OnboardingPromptType`]
.. attribute:: topic
@ -4519,7 +4695,7 @@ AuditLogDiff
.. attribute:: enabled
Whether the automod rule is active or not.
Whether guild onboarding or the automod rule is active or not.
:type: :class:`bool`
@ -4539,7 +4715,7 @@ AuditLogDiff
The trigger for the automod rule.
.. note ::
.. note::
The :attr:`~AutoModTrigger.type` of the trigger may be incorrect.
Some attributes such as :attr:`~AutoModTrigger.keyword_filter`, :attr:`~AutoModTrigger.regex_patterns`,
@ -4551,7 +4727,7 @@ AuditLogDiff
The actions to take when an automod rule is triggered.
:type: List[AutoModRuleAction]
:type: List[:class:`AutoModRuleAction`]
.. attribute:: exempt_roles
@ -4649,6 +4825,71 @@ AuditLogDiff
:type: :class:`float`
.. attribute:: options
The onboarding prompt options associated with this onboarding prompt.
See also :attr:`OnboardingPrompt.options`
:type: List[:class:`OnboardingPromptOption`]
.. attribute:: default_channels
The default channels associated with the onboarding in this guild.
See also :attr:`Onboarding.default_channels`
:type: List[:class:`abc.GuildChannel`, :class:`Object`]
.. attribute:: prompts
The onboarding prompts associated with the onboarding in this guild.
See also :attr:`Onboarding.prompts`
:type: List[:class:`OnboardingPrompt`]
.. attribute:: title
The title of the onboarding prompt.
See also :attr:`OnboardingPrompt.title`
:type: :class:`str`
.. attribute:: single_select
Whether only one prompt option can be selected.
See also :attr:`OnboardingPrompt.single_select`
:type: :class:`bool`
.. attribute:: required
Whether the onboarding prompt is required to complete the onboarding.
See also :attr:`OnboardingPrompt.required`
:type: :class:`bool`
.. attribute:: in_onboarding
Whether this prompt is currently part of the onboarding flow.
See also :attr:`OnboardingPrompt.in_onboarding`
:type: :class:`bool`
.. attribute:: mode
The onboarding constraint mode.
See also :attr:`Onboarding.mode`
:type: :class:`OnboardingMode`
.. this is currently missing the following keys: reason and application_id
I'm not sure how to port these
@ -5272,6 +5513,31 @@ GuildSticker
.. autoclass:: GuildSticker()
:members:
Onboarding
~~~~~~~~~~~
.. attributetable:: Onboarding
.. autoclass:: Onboarding()
:members:
OnboardingPrompt
~~~~~~~~~~~~~~~~~
.. attributetable:: OnboardingPrompt
.. autoclass:: OnboardingPrompt()
:members:
OnboardingPromptOption
~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: OnboardingPromptOption
.. autoclass:: OnboardingPromptOption()
:members:
BaseSoundboardSound
~~~~~~~~~~~~~~~~~~~~~~~
@ -5480,8 +5746,6 @@ PollAnswer
.. autoclass:: PollAnswer()
:members:
.. _discord_api_data:
MessageSnapshot
~~~~~~~~~~~~~~~~~
@ -5506,6 +5770,16 @@ PrimaryGuild
.. autoclass:: PrimaryGuild()
:members:
CallMessage
~~~~~~~~~~~~~~~~~~~
.. attributetable:: CallMessage
.. autoclass:: CallMessage()
:members:
.. _discord_api_data:
Data Classes
--------------
@ -5817,12 +6091,21 @@ PollMedia
.. autoclass:: PollMedia
:members:
CallMessage
~~~~~~~~~~~~~~~~~~~
UnfurledMediaItem
~~~~~~~~~~~~~~~~~
.. attributetable:: CallMessage
.. attributetable:: UnfurledMediaItem
.. autoclass:: CallMessage()
.. autoclass:: UnfurledMediaItem
:members:
MediaGalleryItem
~~~~~~~~~~~~~~~~
.. attributetable:: MediaGalleryItem
.. autoclass:: MediaGalleryItem
:members:

79
docs/faq.rst

@ -500,3 +500,82 @@ My bot's commands are not showing up!
``https://discord.com/oauth2/authorize?client_id=<client id>&scope=applications.commands+bot``.
Alternatively, if you use :func:`utils.oauth_url`, you can call the function as such:
``oauth_url(<other options>, scopes=("bot", "applications.commands"))``.
How do I restrict a command to a specific guild?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To restrict an application command to one or more guilds, you must register it as a **guild command** instead of a
global command. Guild commands are only available in the specified guild(s).
The most straightforward way is to use the :meth:`~app_commands.guilds` decorator on your command or GroupCog.
``123456789012345678`` should be replaced with the actual guild ID you want to restrict the command to.
.. code-block:: python3
@app_commands.command() # or @tree.command()
@app_commands.guilds(123456789012345678) # or @app_commands.guilds(discord.Object(123456789012345678))
async def ping(interaction: Interaction):
await interaction.response.send_message("Pong!")
# or GroupCog (applies to all subcommands):
@app_commands.guilds(123456789012345678)
class MyGroup(commands.GroupCog):
@app_commands.command()
async def pong(self, interaction: Interaction):
await interaction.response.send_message("Ping!")
After that, you must :meth:`~app_commands.CommandTree.sync` the command tree for each guild:
.. code-block:: python3
await tree.sync(guild=discord.Object(123456789012345678))
Other methods to restrict commands to specific guilds include:
- Using the ``guild`` or ``guilds`` argument in the :meth:`~app_commands.CommandTree.command` decorator:
.. code-block:: python3
@tree.command(guild=discord.Object(123456789012345678))
async def ping(interaction: Interaction):
await interaction.response.send_message("Pong!")
- Adding commands with :meth:`~app_commands.CommandTree.add_command` and specifying ``guild`` or ``guilds``:
.. code-block:: python3
@app_commands.command()
async def ping(interaction: Interaction):
await interaction.response.send_message("Pong!")
tree.add_command(ping, guild=discord.Object(123456789012345678))
.. warning::
Do not combine this method with the :meth:`~app_commands.CommandTree.command` decorator,
as it will cause duplicate commands.
- Using ``guild`` or ``guilds`` in :meth:`~ext.commands.Bot.add_cog`:
This is mainly for :class:`~ext.commands.GroupCog`, but also works for cogs with application commands.
Note: This does not work with hybrid app commands (:issue:`9366`).
.. code-block:: python3
class MyCog(commands.Cog):
@app_commands.command()
async def ping(self, interaction: Interaction):
await interaction.response.send_message("Pong!")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(MyCog(...), guild=discord.Object(123456789012345678))
- Using :meth:`~app_commands.CommandTree.copy_global_to`:
This copies all global commands to a specific guild. This is mainly for development purposes.
.. code-block:: python3
tree.copy_global_to(guild=discord.Object(123456789012345678))

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save