Browse Source

Merge branch 'master' into feature/guild_onboarding

pull/9260/head
Josh 2 years ago
parent
commit
a0db097662
  1. 2
      .github/workflows/lint.yml
  2. 1
      discord/__init__.py
  3. 9
      discord/abc.py
  4. 6
      discord/activity.py
  5. 5
      discord/app_commands/commands.py
  6. 35
      discord/app_commands/errors.py
  7. 7
      discord/app_commands/transformers.py
  8. 2
      discord/app_commands/translator.py
  9. 2
      discord/app_commands/tree.py
  10. 133
      discord/audit_logs.py
  11. 118
      discord/automod.py
  12. 40
      discord/channel.py
  13. 296
      discord/client.py
  14. 3
      discord/colour.py
  15. 86
      discord/components.py
  16. 275
      discord/enums.py
  17. 14
      discord/ext/commands/bot.py
  18. 24
      discord/ext/commands/cog.py
  19. 2
      discord/ext/commands/context.py
  20. 6
      discord/ext/commands/core.py
  21. 39
      discord/ext/commands/errors.py
  22. 4
      discord/ext/commands/flags.py
  23. 28
      discord/ext/commands/hybrid.py
  24. 11
      discord/ext/commands/parameters.py
  25. 241
      discord/flags.py
  26. 86
      discord/gateway.py
  27. 35
      discord/guild.py
  28. 84
      discord/http.py
  29. 48
      discord/interactions.py
  30. 3
      discord/invite.py
  31. 4
      discord/member.py
  32. 10
      discord/message.py
  33. 2
      discord/oggparse.py
  34. 64
      discord/opus.py
  35. 49
      discord/permissions.py
  36. 133
      discord/player.py
  37. 44
      discord/raw_models.py
  38. 24
      discord/reaction.py
  39. 11
      discord/role.py
  40. 4
      discord/shard.py
  41. 200
      discord/sku.py
  42. 29
      discord/state.py
  43. 11
      discord/team.py
  44. 21
      discord/template.py
  45. 11
      discord/types/audit_log.py
  46. 1
      discord/types/automod.py
  47. 17
      discord/types/channel.py
  48. 11
      discord/types/components.py
  49. 8
      discord/types/gateway.py
  50. 3
      discord/types/interactions.py
  51. 9
      discord/types/message.py
  52. 1
      discord/types/role.py
  53. 52
      discord/types/sku.py
  54. 2
      discord/types/sticker.py
  55. 3
      discord/types/team.py
  56. 2
      discord/types/user.py
  57. 1
      discord/ui/__init__.py
  58. 209
      discord/ui/dynamic.py
  59. 33
      discord/ui/item.py
  60. 256
      discord/ui/select.py
  61. 94
      discord/ui/view.py
  62. 20
      discord/utils.py
  63. 361
      discord/voice_client.py
  64. 615
      discord/voice_state.py
  65. 20
      docs/_static/style.css
  66. 285
      docs/api.rst
  67. 1
      docs/conf.py
  68. 17
      docs/interactions/api.rst
  69. 3753
      docs/locale/ja/LC_MESSAGES/api.po
  70. 64
      docs/locale/ja/LC_MESSAGES/discord.po
  71. 277
      docs/locale/ja/LC_MESSAGES/ext/commands/api.po
  72. 4
      docs/locale/ja/LC_MESSAGES/ext/commands/cogs.po
  73. 54
      docs/locale/ja/LC_MESSAGES/ext/commands/commands.po
  74. 8
      docs/locale/ja/LC_MESSAGES/ext/commands/extensions.po
  75. 4
      docs/locale/ja/LC_MESSAGES/ext/commands/index.po
  76. 4
      docs/locale/ja/LC_MESSAGES/ext/tasks/index.po
  77. 4
      docs/locale/ja/LC_MESSAGES/faq.po
  78. 4
      docs/locale/ja/LC_MESSAGES/index.po
  79. 4
      docs/locale/ja/LC_MESSAGES/intents.po
  80. 138
      docs/locale/ja/LC_MESSAGES/interactions/api.po
  81. 14
      docs/locale/ja/LC_MESSAGES/intro.po
  82. 4
      docs/locale/ja/LC_MESSAGES/logging.po
  83. 608
      docs/locale/ja/LC_MESSAGES/migrating.po
  84. 4
      docs/locale/ja/LC_MESSAGES/migrating_to_async.po
  85. 4
      docs/locale/ja/LC_MESSAGES/migrating_to_v1.po
  86. 4
      docs/locale/ja/LC_MESSAGES/quickstart.po
  87. 4
      docs/locale/ja/LC_MESSAGES/sphinx.po
  88. 4
      docs/locale/ja/LC_MESSAGES/version_guarantees.po
  89. 2002
      docs/locale/ja/LC_MESSAGES/whats_new.po
  90. 6
      docs/logging.rst
  91. 17
      docs/whats_new.rst
  92. 15
      examples/advanced_startup.py
  93. 98
      examples/views/dynamic_counter.py
  94. 45
      examples/views/persistent.py
  95. 1
      requirements.txt
  96. 3
      setup.py
  97. 110
      tests/test_app_commands_group.py

2
.github/workflows/lint.yml

@ -38,7 +38,7 @@ jobs:
- name: Run Pyright
uses: jakebailey/pyright-action@v1
with:
version: '1.1.289'
version: '1.1.316'
warnings: false
no-comments: ${{ matrix.python-version != '3.x' }}

1
discord/__init__.py

@ -41,6 +41,7 @@ from .integrations import *
from .invite import *
from .template import *
from .welcome_screen import *
from .sku import *
from .widget import *
from .object import *
from .reaction import *

9
discord/abc.py

@ -48,7 +48,7 @@ from typing import (
from .object import OLDEST_OBJECT, Object
from .context_managers import Typing
from .enums import ChannelType
from .enums import ChannelType, InviteTarget
from .errors import ClientException
from .mentions import AllowedMentions
from .permissions import PermissionOverwrite, Permissions
@ -93,7 +93,6 @@ if TYPE_CHECKING:
StageChannel,
)
from .threads import Thread
from .enums import InviteTarget
from .ui.view import View
from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload,
@ -1246,6 +1245,8 @@ class GuildChannel:
:class:`~discord.Invite`
The invite that was created.
"""
if target_type is InviteTarget.unknown:
raise ValueError('Cannot create invite with an unknown target type')
data = await self._state.http.create_invite(
self.id,
@ -1841,7 +1842,7 @@ class Connectable(Protocol):
async def connect(
self,
*,
timeout: float = 60.0,
timeout: float = 30.0,
reconnect: bool = True,
cls: Callable[[Client, Connectable], T] = VoiceClient,
self_deaf: bool = False,
@ -1857,7 +1858,7 @@ class Connectable(Protocol):
Parameters
-----------
timeout: :class:`float`
The timeout in seconds to wait for the voice endpoint.
The timeout in seconds to wait the connection to complete.
reconnect: :class:`bool`
Whether the bot should automatically attempt
a reconnect if a part of the handshake fails

6
discord/activity.py

@ -732,10 +732,12 @@ class CustomActivity(BaseActivity):
__slots__ = ('name', 'emoji', 'state')
def __init__(self, name: Optional[str], *, emoji: Optional[PartialEmoji] = None, **extra: Any) -> None:
def __init__(
self, name: Optional[str], *, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any
) -> None:
super().__init__(**extra)
self.name: Optional[str] = name
self.state: Optional[str] = extra.pop('state', None)
self.state: Optional[str] = extra.pop('state', name)
if self.name == 'Custom Status':
self.name = self.state

5
discord/app_commands/commands.py

@ -976,7 +976,7 @@ class Command(Generic[GroupT, P, T]):
if self.binding is not None:
check: Optional[Check] = getattr(self.binding, 'interaction_check', None)
if check:
ret = await maybe_coroutine(check, interaction) # type: ignore # Probable pyright bug
ret = await maybe_coroutine(check, interaction)
if not ret:
return False
@ -1548,6 +1548,9 @@ class Group:
if not self.description:
raise TypeError('groups must have a description')
if not self.name:
raise TypeError('groups must have a name')
self.parent: Optional[Group] = parent
self.module: Optional[str]
if cls.__discord_app_commands_has_module__:

35
discord/app_commands/errors.py

@ -28,6 +28,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union
from ..enums import AppCommandOptionType, AppCommandType, Locale
from ..errors import DiscordException, HTTPException, _flatten_error_dict
from ..utils import _human_join
__all__ = (
'AppCommandError',
@ -242,13 +243,7 @@ class MissingAnyRole(CheckFailure):
def __init__(self, missing_roles: SnowflakeList) -> None:
self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
fmt = _human_join([f"'{role}'" for role in missing_roles])
message = f'You are missing at least one of the required roles: {fmt}'
super().__init__(message)
@ -271,11 +266,7 @@ class MissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
if len(missing) > 2:
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
fmt = _human_join(missing, final='and')
message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args)
@ -298,11 +289,7 @@ class BotMissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
if len(missing) > 2:
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
fmt = _human_join(missing, final='and')
message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args)
@ -530,8 +517,18 @@ class CommandSyncFailure(AppCommandError, HTTPException):
messages = [f'Failed to upload commands to Discord (HTTP status {self.status}, error code {self.code})']
if self._errors:
for index, inner in self._errors.items():
_get_command_error(index, inner, commands, messages)
# Handle case where the errors dict has no actual chain such as APPLICATION_COMMAND_TOO_LARGE
if len(self._errors) == 1 and '_errors' in self._errors:
errors = self._errors['_errors']
if len(errors) == 1:
extra = errors[0].get('message')
if extra:
messages[0] += f': {extra}'
else:
messages.extend(f'Error {e.get("code", "")}: {e.get("message", "")}' for e in errors)
else:
for index, inner in self._errors.items():
_get_command_error(index, inner, commands, messages)
# Equivalent to super().__init__(...) but skips other constructors
self.args = ('\n'.join(messages),)

7
discord/app_commands/transformers.py

@ -177,8 +177,7 @@ class CommandParameter:
return choice
try:
# ParamSpec doesn't understand that transform is a callable since it's unbound
return await maybe_coroutine(self._annotation.transform, interaction, value) # type: ignore
return await maybe_coroutine(self._annotation.transform, interaction, value)
except AppCommandError:
raise
except Exception as e:
@ -526,7 +525,7 @@ else:
.. versionadded:: 2.0
"""
def __class_getitem__(cls, items) -> _TransformMetadata:
def __class_getitem__(cls, items) -> Transformer:
if not isinstance(items, tuple):
raise TypeError(f'expected tuple for arguments, received {items.__class__.__name__} instead')
@ -571,7 +570,7 @@ else:
await interaction.response.send_message(f'Your value is {value}', ephemeral=True)
"""
def __class_getitem__(cls, obj) -> _TransformMetadata:
def __class_getitem__(cls, obj) -> RangeTransformer:
if not isinstance(obj, tuple):
raise TypeError(f'expected tuple for arguments, received {obj.__class__.__name__} instead')

2
discord/app_commands/translator.py

@ -109,7 +109,7 @@ class TranslationContext(Generic[_L, _D]):
def __init__(self, location: Literal[TranslationContextLocation.other], data: Any) -> None:
...
def __init__(self, location: _L, data: _D) -> None:
def __init__(self, location: _L, data: _D) -> None: # type: ignore # pyright doesn't like the overloads
self.location: _L = location
self.data: _D = data

2
discord/app_commands/tree.py

@ -1240,7 +1240,7 @@ class CommandTree(Generic[ClientT]):
await command._invoke_autocomplete(interaction, focused, namespace)
except Exception:
# Suppress exception since it can't be handled anyway.
pass
_log.exception('Ignoring exception in autocomplete for %r', command.qualified_name)
return

133
discord/audit_logs.py

@ -33,7 +33,7 @@ from .invite import Invite
from .mixins import Hashable
from .object import Object
from .permissions import PermissionOverwrite, Permissions
from .automod import AutoModTrigger, AutoModRuleAction, AutoModPresets, AutoModRule
from .automod import AutoModTrigger, AutoModRuleAction, AutoModRule
from .role import Role
from .emoji import Emoji
from .partial_emoji import PartialEmoji
@ -62,6 +62,7 @@ if TYPE_CHECKING:
from .types.audit_log import (
AuditLogChange as AuditLogChangePayload,
AuditLogEntry as AuditLogEntryPayload,
_AuditLogChange_TriggerMetadata as AuditLogChangeTriggerMetadataPayload,
)
from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload,
@ -72,7 +73,7 @@ if TYPE_CHECKING:
from .types.role import Role as RolePayload
from .types.snowflake import Snowflake
from .types.command import ApplicationCommandPermissions
from .types.automod import AutoModerationTriggerMetadata, AutoModerationAction
from .types.automod import AutoModerationAction
from .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload
from .user import User
from .app_commands import AppCommand
@ -232,39 +233,6 @@ def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]
return _transform
def _transform_automod_trigger_metadata(
entry: AuditLogEntry, data: AutoModerationTriggerMetadata
) -> Optional[AutoModTrigger]:
if isinstance(entry.target, AutoModRule):
# Trigger type cannot be changed, so type should be the same before and after updates.
# Avoids checking which keys are in data to guess trigger type
# or returning None if data is empty.
try:
return AutoModTrigger.from_data(type=entry.target.trigger.type.value, data=data)
except Exception:
pass
# Try to infer trigger type from available keys in data
if 'presets' in data:
return AutoModTrigger(
type=enums.AutoModRuleTriggerType.keyword_preset,
presets=AutoModPresets._from_value(data['presets']), # type: ignore
allow_list=data.get('allow_list'),
)
elif 'keyword_filter' in data:
return AutoModTrigger(
type=enums.AutoModRuleTriggerType.keyword,
keyword_filter=data['keyword_filter'], # type: ignore
allow_list=data.get('allow_list'),
regex_patterns=data.get('regex_patterns'),
)
elif 'mention_total_limit' in data:
return AutoModTrigger(type=enums.AutoModRuleTriggerType.mention_spam, mention_limit=data['mention_total_limit']) # type: ignore
else:
return AutoModTrigger(type=enums.AutoModRuleTriggerType.spam)
def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAction]) -> List[AutoModRuleAction]:
return [AutoModRuleAction.from_data(action) for action in data]
@ -380,7 +348,6 @@ class AuditLogChanges:
'image_hash': ('cover_image', _transform_cover_image),
'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)),
'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)),
'trigger_metadata': ('trigger', _transform_automod_trigger_metadata),
'actions': (None, _transform_automod_actions),
'exempt_channels': (None, _transform_channels_or_threads),
'exempt_roles': (None, _transform_roles),
@ -429,6 +396,21 @@ class AuditLogChanges:
self._handle_role(self.after, self.before, entry, elem['new_value']) # type: ignore # new_value is a list of roles in this case
continue
# special case for automod trigger
if attr == 'trigger_metadata':
# given full metadata dict
self._handle_trigger_metadata(entry, elem, data) # type: ignore # should be trigger metadata
continue
elif entry.action is enums.AuditLogAction.automod_rule_update and attr.startswith('$'):
# on update, some trigger attributes are keys and formatted as $(add/remove)_{attribute}
action, _, trigger_attr = attr.partition('_')
# new_value should be a list of added/removed strings for keyword_filter, regex_patterns, or allow_list
if action == '$add':
self._handle_trigger_attr_update(self.before, self.after, entry, trigger_attr, elem['new_value']) # type: ignore
elif action == '$remove':
self._handle_trigger_attr_update(self.after, self.before, entry, trigger_attr, elem['new_value']) # type: ignore
continue
try:
key, transformer = self.TRANSFORMERS[attr]
except (ValueError, KeyError):
@ -505,6 +487,76 @@ class AuditLogChanges:
guild = entry.guild
diff.app_command_permissions.append(AppCommandPermissions(data=data, guild=guild, state=state))
def _handle_trigger_metadata(
self,
entry: AuditLogEntry,
data: AuditLogChangeTriggerMetadataPayload,
full_data: List[AuditLogChangePayload],
):
trigger_value: Optional[int] = None
trigger_type: Optional[enums.AutoModRuleTriggerType] = None
# try to get trigger type from before or after
trigger_type = getattr(self.before, 'trigger_type', getattr(self.after, 'trigger_type', None))
if trigger_type is None:
if isinstance(entry.target, AutoModRule):
# Trigger type cannot be changed, so it should be the same before and after updates.
# Avoids checking which keys are in data to guess trigger type
trigger_value = entry.target.trigger.type.value
else:
# found a trigger type from before or after
trigger_value = trigger_type.value
if trigger_value is None:
# try to find trigger type in the full list of changes
_elem = utils.find(lambda elem: elem['key'] == 'trigger_type', full_data)
if _elem is not None:
trigger_value = _elem.get('old_value', _elem.get('new_value')) # type: ignore # trigger type values should be int
if trigger_value is None:
# try to infer trigger_type from the keys in old or new value
combined = (data.get('old_value') or {}).keys() | (data.get('new_value') or {}).keys()
if not combined:
trigger_value = enums.AutoModRuleTriggerType.spam.value
elif 'presets' in combined:
trigger_value = enums.AutoModRuleTriggerType.keyword_preset.value
elif 'keyword_filter' in combined or 'regex_patterns' in combined:
trigger_value = enums.AutoModRuleTriggerType.keyword.value
elif 'mention_total_limit' in combined or 'mention_raid_protection_enabled' in combined:
trigger_value = enums.AutoModRuleTriggerType.mention_spam.value
else:
# some unknown type
trigger_value = -1
self.before.trigger = AutoModTrigger.from_data(trigger_value, data.get('old_value'))
self.after.trigger = AutoModTrigger.from_data(trigger_value, data.get('new_value'))
def _handle_trigger_attr_update(
self, first: AuditLogDiff, second: AuditLogDiff, entry: AuditLogEntry, attr: str, data: List[str]
):
self._create_trigger(first, entry)
trigger = self._create_trigger(second, entry)
try:
# guard unexpecte non list attributes or non iterable data
getattr(trigger, attr).extend(data)
except (AttributeError, TypeError):
pass
def _create_trigger(self, diff: AuditLogDiff, entry: AuditLogEntry) -> AutoModTrigger:
# check if trigger has already been created
if not hasattr(diff, 'trigger'):
# create a trigger
if isinstance(entry.target, AutoModRule):
# get trigger type from the automod rule
trigger_type = entry.target.trigger.type
else:
# unknown trigger type
trigger_type = enums.try_enum(enums.AutoModRuleTriggerType, -1)
diff.trigger = AutoModTrigger(type=trigger_type)
return diff.trigger
class _AuditLogProxy:
def __init__(self, **kwargs: Any) -> None:
@ -545,6 +597,10 @@ class _AuditLogProxyAutoModAction(_AuditLogProxy):
channel: Optional[Union[abc.GuildChannel, Thread]]
class _AuditLogProxyMemberKickOrMemberRoleUpdate(_AuditLogProxy):
integration_type: Optional[str]
class AuditLogEntry(Hashable):
r"""Represents an Audit Log entry.
@ -631,6 +687,7 @@ class AuditLogEntry(Hashable):
_AuditLogProxyStageInstanceAction,
_AuditLogProxyMessageBulkDelete,
_AuditLogProxyAutoModAction,
_AuditLogProxyMemberKickOrMemberRoleUpdate,
Member, User, None, PartialIntegration,
Role, Object
] = None
@ -655,6 +712,10 @@ class AuditLogEntry(Hashable):
elif self.action is enums.AuditLogAction.message_bulk_delete:
# The bulk message delete action has the number of messages deleted
self.extra = _AuditLogProxyMessageBulkDelete(count=int(extra['count']))
elif self.action in (enums.AuditLogAction.kick, enums.AuditLogAction.member_role_update):
# The member kick action has a dict with some information
integration_type = extra.get('integration_type')
self.extra = _AuditLogProxyMemberKickOrMemberRoleUpdate(integration_type=integration_type)
elif self.action.name.endswith('pin'):
# the pin actions have a dict with some information
channel_id = int(extra['channel_id'])

118
discord/automod.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, List, Set, Union, Sequence, overload
from typing import TYPE_CHECKING, Any, Dict, Optional, List, Set, Union, Sequence, overload, Literal
from .enums import AutoModRuleTriggerType, AutoModRuleActionType, AutoModRuleEventType, try_enum
from .flags import AutoModPresets
@ -85,36 +85,81 @@ class AutoModRuleAction:
__slots__ = ('type', 'channel_id', 'duration', 'custom_message')
@overload
def __init__(self, *, channel_id: Optional[int] = ...) -> None:
def __init__(self, *, channel_id: int = ...) -> None:
...
@overload
def __init__(self, *, duration: Optional[datetime.timedelta] = ...) -> None:
def __init__(self, *, type: Literal[AutoModRuleActionType.send_alert_message], channel_id: int = ...) -> None:
...
@overload
def __init__(self, *, custom_message: Optional[str] = ...) -> None:
def __init__(self, *, duration: datetime.timedelta = ...) -> None:
...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.timeout], duration: datetime.timedelta = ...) -> None:
...
@overload
def __init__(self, *, custom_message: str = ...) -> None:
...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message]) -> None:
...
@overload
def __init__(self, *, type: Literal[AutoModRuleActionType.block_message], custom_message: Optional[str] = ...) -> None:
...
@overload
def __init__(
self,
*,
type: Optional[AutoModRuleActionType] = ...,
channel_id: Optional[int] = ...,
duration: Optional[datetime.timedelta] = ...,
custom_message: Optional[str] = ...,
) -> None:
...
def __init__(
self,
*,
type: Optional[AutoModRuleActionType] = None,
channel_id: Optional[int] = None,
duration: Optional[datetime.timedelta] = None,
custom_message: Optional[str] = None,
) -> None:
self.channel_id: Optional[int] = channel_id
self.duration: Optional[datetime.timedelta] = duration
self.custom_message: Optional[str] = custom_message
if sum(v is None for v in (channel_id, duration, custom_message)) < 2:
raise ValueError('Only one of channel_id, duration, or custom_message can be passed.')
self.type: AutoModRuleActionType = AutoModRuleActionType.block_message
if channel_id:
self.type: AutoModRuleActionType
self.channel_id: Optional[int] = None
self.duration: Optional[datetime.timedelta] = None
self.custom_message: Optional[str] = None
if type is not None:
self.type = type
elif channel_id is not None:
self.type = AutoModRuleActionType.send_alert_message
elif duration:
elif duration is not None:
self.type = AutoModRuleActionType.timeout
else:
self.type = AutoModRuleActionType.block_message
if self.type is AutoModRuleActionType.send_alert_message:
if channel_id is None:
raise ValueError('channel_id cannot be None if type is send_alert_message')
self.channel_id = channel_id
if self.type is AutoModRuleActionType.timeout:
if duration is None:
raise ValueError('duration cannot be None set if type is timeout')
self.duration = duration
if self.type is AutoModRuleActionType.block_message:
self.custom_message = custom_message
def __repr__(self) -> str:
return f'<AutoModRuleAction type={self.type.value} channel={self.channel_id} duration={self.duration}>'
@ -127,7 +172,11 @@ class AutoModRuleAction:
elif data['type'] == AutoModRuleActionType.send_alert_message.value:
channel_id = int(data['metadata']['channel_id'])
return cls(channel_id=channel_id)
return cls(custom_message=data.get('metadata', {}).get('custom_message'))
elif data['type'] == AutoModRuleActionType.block_message.value:
custom_message = data.get('metadata', {}).get('custom_message')
return cls(type=AutoModRuleActionType.block_message, custom_message=custom_message)
return cls(type=AutoModRuleActionType.block_member_interactions)
def to_dict(self) -> Dict[str, Any]:
ret = {'type': self.type.value, 'metadata': {}}
@ -155,7 +204,11 @@ class AutoModTrigger:
+-----------------------------------------------+------------------------------------------------+
| :attr:`AutoModRuleTriggerType.keyword_preset` | :attr:`presets`\, :attr:`allow_list` |
+-----------------------------------------------+------------------------------------------------+
| :attr:`AutoModRuleTriggerType.mention_spam` | :attr:`mention_limit` |
| :attr:`AutoModRuleTriggerType.mention_spam` | :attr:`mention_limit`, |
| | :attr:`mention_raid_protection` |
+-----------------------------------------------+------------------------------------------------+
| :attr:`AutoModRuleTriggerType.member_profile` | :attr:`keyword_filter`, :attr:`regex_patterns`,|
| | :attr:`allow_list` |
+-----------------------------------------------+------------------------------------------------+
.. versionadded:: 2.0
@ -165,8 +218,8 @@ class AutoModTrigger:
type: :class:`AutoModRuleTriggerType`
The type of trigger.
keyword_filter: List[:class:`str`]
The list of strings that will trigger the keyword filter. Maximum of 1000.
Keywords can only be up to 60 characters in length.
The list of strings that will trigger the filter.
Maximum of 1000. Keywords can only be up to 60 characters in length.
This could be combined with :attr:`regex_patterns`.
regex_patterns: List[:class:`str`]
@ -185,6 +238,10 @@ class AutoModTrigger:
mention_limit: :class:`int`
The total number of user and role mentions a message can contain.
Has a maximum of 50.
mention_raid_protection: :class:`bool`
Whether mention raid protection is enabled or not.
.. versionadded:: 2.4
"""
__slots__ = (
@ -194,6 +251,7 @@ class AutoModTrigger:
'allow_list',
'mention_limit',
'regex_patterns',
'mention_raid_protection',
)
def __init__(
@ -205,9 +263,13 @@ class AutoModTrigger:
allow_list: Optional[List[str]] = None,
mention_limit: Optional[int] = None,
regex_patterns: Optional[List[str]] = None,
mention_raid_protection: Optional[bool] = None,
) -> None:
if type is None and sum(arg is not None for arg in (keyword_filter or regex_patterns, presets, mention_limit)) > 1:
raise ValueError('Please pass only one of keyword_filter, regex_patterns, presets, or mention_limit.')
unique_args = (keyword_filter or regex_patterns, presets, mention_limit or mention_raid_protection)
if type is None and sum(arg is not None for arg in unique_args) > 1:
raise ValueError(
'Please pass only one of keyword_filter/regex_patterns, presets, or mention_limit/mention_raid_protection.'
)
if type is not None:
self.type = type
@ -215,17 +277,18 @@ class AutoModTrigger:
self.type = AutoModRuleTriggerType.keyword
elif presets is not None:
self.type = AutoModRuleTriggerType.keyword_preset
elif mention_limit is not None:
elif mention_limit is not None or mention_raid_protection is not None:
self.type = AutoModRuleTriggerType.mention_spam
else:
raise ValueError(
'Please pass the trigger type explicitly if not using keyword_filter, presets, or mention_limit.'
'Please pass the trigger type explicitly if not using keyword_filter, regex_patterns, presets, mention_limit, or mention_raid_protection.'
)
self.keyword_filter: List[str] = keyword_filter if keyword_filter is not None else []
self.presets: AutoModPresets = presets if presets is not None else AutoModPresets()
self.allow_list: List[str] = allow_list if allow_list is not None else []
self.mention_limit: int = mention_limit if mention_limit is not None else 0
self.mention_raid_protection: bool = mention_raid_protection if mention_raid_protection is not None else False
self.regex_patterns: List[str] = regex_patterns if regex_patterns is not None else []
def __repr__(self) -> str:
@ -241,7 +304,7 @@ class AutoModTrigger:
type_ = try_enum(AutoModRuleTriggerType, type)
if data is None:
return cls(type=type_)
elif type_ is AutoModRuleTriggerType.keyword:
elif type_ in (AutoModRuleTriggerType.keyword, AutoModRuleTriggerType.member_profile):
return cls(
type=type_,
keyword_filter=data.get('keyword_filter'),
@ -253,12 +316,16 @@ class AutoModTrigger:
type=type_, presets=AutoModPresets._from_value(data.get('presets', [])), allow_list=data.get('allow_list')
)
elif type_ is AutoModRuleTriggerType.mention_spam:
return cls(type=type_, mention_limit=data.get('mention_total_limit'))
return cls(
type=type_,
mention_limit=data.get('mention_total_limit'),
mention_raid_protection=data.get('mention_raid_protection_enabled'),
)
else:
return cls(type=type_)
def to_metadata_dict(self) -> Optional[Dict[str, Any]]:
if self.type is AutoModRuleTriggerType.keyword:
if self.type in (AutoModRuleTriggerType.keyword, AutoModRuleTriggerType.member_profile):
return {
'keyword_filter': self.keyword_filter,
'regex_patterns': self.regex_patterns,
@ -267,7 +334,10 @@ class AutoModTrigger:
elif self.type is AutoModRuleTriggerType.keyword_preset:
return {'presets': self.presets.to_array(), 'allow_list': self.allow_list}
elif self.type is AutoModRuleTriggerType.mention_spam:
return {'mention_total_limit': self.mention_limit}
return {
'mention_total_limit': self.mention_limit,
'mention_raid_protection_enabled': self.mention_raid_protection,
}
class AutoModRule:
@ -293,6 +363,8 @@ class AutoModRule:
The IDs of the roles that are exempt from the rule.
exempt_channel_ids: Set[:class:`int`]
The IDs of the channels that are exempt from the rule.
event_type: :class:`AutoModRuleEventType`
The type of event that will trigger the the rule.
"""
__slots__ = (

40
discord/channel.py

@ -98,6 +98,7 @@ if TYPE_CHECKING:
CategoryChannel as CategoryChannelPayload,
GroupDMChannel as GroupChannelPayload,
ForumChannel as ForumChannelPayload,
MediaChannel as MediaChannelPayload,
ForumTag as ForumTagPayload,
)
from .types.snowflake import SnowflakeList
@ -776,7 +777,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
self.id,
name=name,
auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration,
type=type.value,
type=type.value, # type: ignore # we're assuming that the user is passing a valid variant
reason=reason,
invitable=invitable,
rate_limit_per_user=slowmode_delay,
@ -1599,6 +1600,7 @@ class StageChannel(VocalGuildChannel):
topic: str,
privacy_level: PrivacyLevel = MISSING,
send_start_notification: bool = False,
scheduled_event: Snowflake = MISSING,
reason: Optional[str] = None,
) -> StageInstance:
"""|coro|
@ -1620,6 +1622,10 @@ class StageChannel(VocalGuildChannel):
You must have :attr:`~Permissions.mention_everyone` to do this.
.. versionadded:: 2.3
scheduled_event: :class:`~discord.abc.Snowflake`
The guild scheduled event associated with the stage instance.
.. versionadded:: 2.4
reason: :class:`str`
The reason the stage instance was created. Shows up on the audit log.
@ -1646,6 +1652,9 @@ class StageChannel(VocalGuildChannel):
payload['privacy_level'] = privacy_level.value
if scheduled_event is not MISSING:
payload['guild_scheduled_event_id'] = scheduled_event.id
payload['send_start_notification'] = send_start_notification
data = await self._state.http.create_stage_instance(**payload, reason=reason)
@ -1971,6 +1980,16 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
ret.sort(key=lambda c: (c.position, c.id))
return ret
@property
def forums(self) -> List[ForumChannel]:
"""List[:class:`ForumChannel`]: Returns the forum channels that are under this category.
.. versionadded:: 2.4
"""
r = [c for c in self.guild.channels if c.category_id == self.id and isinstance(c, ForumChannel)]
r.sort(key=lambda c: (c.position, c.id))
return r
async def create_text_channel(self, name: str, **options: Any) -> TextChannel:
"""|coro|
@ -2192,6 +2211,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
'topic',
'_state',
'_flags',
'_type',
'nsfw',
'category_id',
'position',
@ -2207,9 +2227,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
'_flags',
)
def __init__(self, *, state: ConnectionState, guild: Guild, data: ForumChannelPayload):
def __init__(self, *, state: ConnectionState, guild: Guild, data: Union[ForumChannelPayload, MediaChannelPayload]):
self._state: ConnectionState = state
self.id: int = int(data['id'])
self._type: Literal[15, 16] = data['type']
self._update(guild, data)
def __repr__(self) -> str:
@ -2223,7 +2244,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
def _update(self, guild: Guild, data: ForumChannelPayload) -> None:
def _update(self, guild: Guild, data: Union[ForumChannelPayload, MediaChannelPayload]) -> None:
self.guild: Guild = guild
self.name: str = data['name']
self.category_id: Optional[int] = utils._get_as_snowflake(data, 'parent_id')
@ -2257,8 +2278,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
self._fill_overwrites(data)
@property
def type(self) -> Literal[ChannelType.forum]:
def type(self) -> Literal[ChannelType.forum, ChannelType.media]:
""":class:`ChannelType`: The channel's Discord type."""
if self._type == 16:
return ChannelType.media
return ChannelType.forum
@property
@ -2346,6 +2369,13 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
""":class:`bool`: Checks if the forum is NSFW."""
return self.nsfw
def is_media(self) -> bool:
""":class:`bool`: Checks if the channel is a media channel.
.. versionadded:: 2.4
"""
return self._type == ChannelType.media.value
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> ForumChannel:
return await self._clone_impl(
@ -3304,6 +3334,8 @@ def _guild_channel_factory(channel_type: int):
return StageChannel, value
elif value is ChannelType.forum:
return ForumChannel, value
elif value is ChannelType.media:
return ForumChannel, value
else:
return None, value

296
discord/client.py

@ -48,6 +48,7 @@ from typing import (
import aiohttp
from .sku import SKU, Entitlement
from .user import User, ClientUser
from .invite import Invite
from .template import Template
@ -55,7 +56,7 @@ from .widget import Widget
from .guild import Guild
from .emoji import Emoji
from .channel import _threaded_channel_factory, PartialMessageable
from .enums import ChannelType
from .enums import ChannelType, EntitlementOwnerType
from .mentions import AllowedMentions
from .errors import *
from .enums import Status
@ -72,6 +73,7 @@ from .backoff import ExponentialBackoff
from .webhook import Webhook
from .appinfo import AppInfo
from .ui.view import View
from .ui.dynamic import DynamicItem
from .stage_instance import StageInstance
from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@ -82,7 +84,7 @@ if TYPE_CHECKING:
from typing_extensions import Self
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu
from .app_commands import Command, ContextMenu, MissingApplicationID
from .automod import AutoModAction, AutoModRule
from .channel import DMChannel, GroupChannel
from .ext.commands import AutoShardedBot, Bot, Context, CommandError
@ -111,6 +113,7 @@ if TYPE_CHECKING:
from .scheduled_event import ScheduledEvent
from .threads import ThreadMember
from .types.guild import Guild as GuildPayload
from .ui.item import Item
from .voice_client import VoiceProtocol
from .audit_logs import AuditLogEntry
@ -672,7 +675,6 @@ class Client:
aiohttp.ClientError,
asyncio.TimeoutError,
) as exc:
self.dispatch('disconnect')
if not reconnect:
await self.close()
@ -2241,8 +2243,8 @@ class Client:
Raises
------
Forbidden
You do not have access to the guild.
NotFound
The guild doesn't exist or you got no access to it.
HTTPException
Getting the guild failed.
@ -2630,6 +2632,242 @@ class Client:
# The type checker is not smart enough to figure out the constructor is correct
return cls(state=self._connection, data=data) # type: ignore
async def fetch_skus(self) -> List[SKU]:
"""|coro|
Retrieves the bot's available SKUs.
.. versionadded:: 2.4
Raises
-------
MissingApplicationID
The application ID could not be found.
HTTPException
Retrieving the SKUs failed.
Returns
--------
List[:class:`.SKU`]
The bot's available SKUs.
"""
if self.application_id is None:
raise MissingApplicationID
data = await self.http.get_skus(self.application_id)
return [SKU(state=self._connection, data=sku) for sku in data]
async def fetch_entitlement(self, entitlement_id: int, /) -> Entitlement:
"""|coro|
Retrieves a :class:`.Entitlement` with the specified ID.
.. versionadded:: 2.4
Parameters
-----------
entitlement_id: :class:`int`
The entitlement's ID to fetch from.
Raises
-------
NotFound
An entitlement with this ID does not exist.
MissingApplicationID
The application ID could not be found.
HTTPException
Fetching the entitlement failed.
Returns
--------
:class:`.Entitlement`
The entitlement you requested.
"""
if self.application_id is None:
raise MissingApplicationID
data = await self.http.get_entitlement(self.application_id, entitlement_id)
return Entitlement(state=self._connection, data=data)
async def entitlements(
self,
*,
limit: Optional[int] = 100,
before: Optional[SnowflakeTime] = None,
after: Optional[SnowflakeTime] = None,
skus: Optional[Sequence[Snowflake]] = None,
user: Optional[Snowflake] = None,
guild: Optional[Snowflake] = None,
exclude_ended: bool = False,
) -> AsyncIterator[Entitlement]:
"""Retrieves an :term:`asynchronous iterator` of the :class:`.Entitlement` that applications has.
.. versionadded:: 2.4
Examples
---------
Usage ::
async for entitlement in client.entitlements(limit=100):
print(entitlement.user_id, entitlement.ends_at)
Flattening into a list ::
entitlements = [entitlement async for entitlement in client.entitlements(limit=100)]
# entitlements is now a list of Entitlement...
All parameters are optional.
Parameters
-----------
limit: Optional[:class:`int`]
The number of entitlements to retrieve. If ``None``, it retrieves every entitlement for this application.
Note, however, that this would make it a slow operation. Defaults to ``100``.
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve entitlements before this date or entitlement.
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.
after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve entitlements after this date or entitlement.
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.
skus: Optional[Sequence[:class:`~discord.abc.Snowflake`]]
A list of SKUs to filter by.
user: Optional[:class:`~discord.abc.Snowflake`]
The user to filter by.
guild: Optional[:class:`~discord.abc.Snowflake`]
The guild to filter by.
exclude_ended: :class:`bool`
Whether to exclude ended entitlements. Defaults to ``False``.
Raises
-------
MissingApplicationID
The application ID could not be found.
HTTPException
Fetching the entitlements failed.
TypeError
Both ``after`` and ``before`` were provided, as Discord does not
support this type of pagination.
Yields
--------
:class:`.Entitlement`
The entitlement with the application.
"""
if self.application_id is None:
raise MissingApplicationID
if before is not None and after is not None:
raise TypeError('entitlements pagination does not support both before and after')
# This endpoint paginates in ascending order.
endpoint = self.http.get_entitlements
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
before_id = before.id if before else None
data = await endpoint(
self.application_id, # type: ignore # We already check for None above
limit=retrieve,
before=before_id,
sku_ids=[sku.id for sku in skus] if skus else None,
user_id=user.id if user else None,
guild_id=guild.id if guild else None,
exclude_ended=exclude_ended,
)
if data:
if limit is not None:
limit -= len(data)
before = Object(id=int(data[0]['id']))
return data, before, limit
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
after_id = after.id if after else None
data = await endpoint(
self.application_id, # type: ignore # We already check for None above
limit=retrieve,
after=after_id,
sku_ids=[sku.id for sku in skus] if skus else None,
user_id=user.id if user else None,
guild_id=guild.id if guild else None,
exclude_ended=exclude_ended,
)
if data:
if limit is not None:
limit -= len(data)
after = Object(id=int(data[-1]['id']))
return data, after, limit
if isinstance(before, datetime.datetime):
before = Object(id=utils.time_snowflake(before, high=False))
if isinstance(after, datetime.datetime):
after = Object(id=utils.time_snowflake(after, high=True))
if before:
strategy, state = _before_strategy, before
else:
strategy, state = _after_strategy, after
while True:
retrieve = 100 if limit is None else min(limit, 100)
if retrieve < 1:
return
data, state, limit = await strategy(retrieve, state, limit)
# Terminate loop on next iteration; there's no data left after this
if len(data) < 1000:
limit = 0
for e in data:
yield Entitlement(self._connection, e)
async def create_entitlement(
self,
sku: Snowflake,
owner: Snowflake,
owner_type: EntitlementOwnerType,
) -> None:
"""|coro|
Creates a test :class:`.Entitlement` for the application.
.. versionadded:: 2.4
Parameters
-----------
sku: :class:`~discord.abc.Snowflake`
The SKU to create the entitlement for.
owner: :class:`~discord.abc.Snowflake`
The ID of the owner.
owner_type: :class:`.EntitlementOwnerType`
The type of the owner.
Raises
-------
MissingApplicationID
The application ID could not be found.
NotFound
The SKU or owner could not be found.
HTTPException
Creating the entitlement failed.
"""
if self.application_id is None:
raise MissingApplicationID
await self.http.create_entitlement(self.application_id, sku.id, owner.id, owner_type.value)
async def fetch_premium_sticker_packs(self) -> List[StickerPack]:
"""|coro|
@ -2678,6 +2916,54 @@ class Client:
data = await state.http.start_private_message(user.id)
return state.add_dm_channel(data)
def add_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
r"""Registers :class:`~discord.ui.DynamicItem` classes for persistent listening.
This method accepts *class types* rather than instances.
.. versionadded:: 2.4
Parameters
-----------
\*items: Type[:class:`~discord.ui.DynamicItem`]
The classes of dynamic items to add.
Raises
-------
TypeError
A class is not a subclass of :class:`~discord.ui.DynamicItem`.
"""
for item in items:
if not issubclass(item, DynamicItem):
raise TypeError(f'expected subclass of DynamicItem not {item.__name__}')
self._connection.store_dynamic_items(*items)
def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
r"""Removes :class:`~discord.ui.DynamicItem` classes from persistent listening.
This method accepts *class types* rather than instances.
.. versionadded:: 2.4
Parameters
-----------
\*items: Type[:class:`~discord.ui.DynamicItem`]
The classes of dynamic items to remove.
Raises
-------
TypeError
A class is not a subclass of :class:`~discord.ui.DynamicItem`.
"""
for item in items:
if not issubclass(item, DynamicItem):
raise TypeError(f'expected subclass of DynamicItem not {item.__name__}')
self._connection.remove_dynamic_items(*items)
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
"""Registers a :class:`~discord.ui.View` for persistent listening.

3
discord/colour.py

@ -196,6 +196,9 @@ class Colour:
The string could not be converted into a colour.
"""
if not value:
raise ValueError('unknown colour format given')
if value[0] == '#':
return parse_hex_number(value[1:])

86
discord/components.py

@ -25,7 +25,7 @@ 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
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType
from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag
@ -40,8 +40,10 @@ if TYPE_CHECKING:
ActionRow as ActionRowPayload,
TextInput as TextInputPayload,
ActionRowChildComponent as ActionRowChildComponentPayload,
SelectDefaultValues as SelectDefaultValuesPayload,
)
from .emoji import Emoji
from .abc import Snowflake
ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput']
@ -53,6 +55,7 @@ __all__ = (
'SelectMenu',
'SelectOption',
'TextInput',
'SelectDefaultValue',
)
@ -263,6 +266,7 @@ class SelectMenu(Component):
'options',
'disabled',
'channel_types',
'default_values',
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
@ -276,10 +280,13 @@ class SelectMenu(Component):
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', [])
]
def to_dict(self) -> SelectMenuPayload:
payload: SelectMenuPayload = {
'type': self.type.value,
'type': self.type.value, # type: ignore # we know this is a select menu.
'custom_id': self.custom_id,
'min_values': self.min_values,
'max_values': self.max_values,
@ -291,6 +298,8 @@ class SelectMenu(Component):
payload['options'] = [op.to_dict() for op in self.options]
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]
return payload
@ -512,6 +521,79 @@ class TextInput(Component):
return self.value
class SelectDefaultValue:
"""Represents a select menu's default value.
These can be created by users.
.. versionadded:: 2.4
Parameters
-----------
id: :class:`int`
The id of a role, user, or channel.
type: :class:`SelectDefaultValueType`
The type of value that ``id`` represents.
"""
def __init__(
self,
*,
id: int,
type: SelectDefaultValueType,
) -> None:
self.id: int = id
self._type: SelectDefaultValueType = type
@property
def type(self) -> SelectDefaultValueType:
return self._type
@type.setter
def type(self, value: SelectDefaultValueType) -> None:
if not isinstance(value, SelectDefaultValueType):
raise TypeError(f'expected SelectDefaultValueType, received {value.__class__.__name__} instead')
self._type = value
def __repr__(self) -> str:
return f'<SelectDefaultValue id={self.id!r} type={self.type!r}>'
@classmethod
def from_dict(cls, data: SelectDefaultValuesPayload) -> SelectDefaultValue:
return cls(
id=data['id'],
type=try_enum(SelectDefaultValueType, data['type']),
)
def to_dict(self) -> SelectDefaultValuesPayload:
return {
'id': self.id,
'type': self._type.value,
}
@classmethod
def from_channel(cls, channel: Snowflake, /) -> Self:
return cls(
id=channel.id,
type=SelectDefaultValueType.channel,
)
@classmethod
def from_role(cls, role: Snowflake, /) -> Self:
return cls(
id=role.id,
type=SelectDefaultValueType.role,
)
@classmethod
def from_user(cls, user: Snowflake, /) -> Self:
return cls(
id=user.id,
type=SelectDefaultValueType.user,
)
@overload
def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]:
...

275
discord/enums.py

@ -42,6 +42,7 @@ __all__ = (
'ActivityType',
'NotificationLevel',
'TeamMembershipState',
'TeamMemberRole',
'WebhookType',
'ExpireBehaviour',
'ExpireBehavior',
@ -68,6 +69,10 @@ __all__ = (
'AutoModRuleActionType',
'ForumLayoutType',
'ForumOrderType',
'SelectDefaultValueType',
'SKUType',
'EntitlementType',
'EntitlementOwnerType',
'OnboardingPromptType',
'OnboardingMode',
)
@ -204,6 +209,7 @@ class ChannelType(Enum):
private_thread = 12
stage_voice = 13
forum = 15
media = 16
def __str__(self) -> str:
return self.name
@ -316,128 +322,133 @@ class AuditLogActionCategory(Enum):
class AuditLogAction(Enum):
# fmt: off
guild_update = 1
channel_create = 10
channel_update = 11
channel_delete = 12
overwrite_create = 13
overwrite_update = 14
overwrite_delete = 15
kick = 20
member_prune = 21
ban = 22
unban = 23
member_update = 24
member_role_update = 25
member_move = 26
member_disconnect = 27
bot_add = 28
role_create = 30
role_update = 31
role_delete = 32
invite_create = 40
invite_update = 41
invite_delete = 42
webhook_create = 50
webhook_update = 51
webhook_delete = 52
emoji_create = 60
emoji_update = 61
emoji_delete = 62
message_delete = 72
message_bulk_delete = 73
message_pin = 74
message_unpin = 75
integration_create = 80
integration_update = 81
integration_delete = 82
stage_instance_create = 83
stage_instance_update = 84
stage_instance_delete = 85
sticker_create = 90
sticker_update = 91
sticker_delete = 92
scheduled_event_create = 100
scheduled_event_update = 101
scheduled_event_delete = 102
thread_create = 110
thread_update = 111
thread_delete = 112
app_command_permission_update = 121
automod_rule_create = 140
automod_rule_update = 141
automod_rule_delete = 142
automod_block_message = 143
automod_flag_message = 144
automod_timeout_member = 145
onboarding_question_create = 163
onboarding_question_update = 164
onboarding_update = 167
server_guide_create = 190
server_guide_update = 191
guild_update = 1
channel_create = 10
channel_update = 11
channel_delete = 12
overwrite_create = 13
overwrite_update = 14
overwrite_delete = 15
kick = 20
member_prune = 21
ban = 22
unban = 23
member_update = 24
member_role_update = 25
member_move = 26
member_disconnect = 27
bot_add = 28
role_create = 30
role_update = 31
role_delete = 32
invite_create = 40
invite_update = 41
invite_delete = 42
webhook_create = 50
webhook_update = 51
webhook_delete = 52
emoji_create = 60
emoji_update = 61
emoji_delete = 62
message_delete = 72
message_bulk_delete = 73
message_pin = 74
message_unpin = 75
integration_create = 80
integration_update = 81
integration_delete = 82
stage_instance_create = 83
stage_instance_update = 84
stage_instance_delete = 85
sticker_create = 90
sticker_update = 91
sticker_delete = 92
scheduled_event_create = 100
scheduled_event_update = 101
scheduled_event_delete = 102
thread_create = 110
thread_update = 111
thread_delete = 112
app_command_permission_update = 121
automod_rule_create = 140
automod_rule_update = 141
automod_rule_delete = 142
automod_block_message = 143
automod_flag_message = 144
automod_timeout_member = 145
creator_monetization_request_created = 150
creator_monetization_terms_accepted = 151
onboarding_question_create = 163
onboarding_question_update = 164
onboarding_update = 167
server_guide_create = 190
server_guide_update = 191
# fmt: on
@property
def category(self) -> Optional[AuditLogActionCategory]:
# fmt: off
lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = {
AuditLogAction.guild_update: AuditLogActionCategory.update,
AuditLogAction.channel_create: AuditLogActionCategory.create,
AuditLogAction.channel_update: AuditLogActionCategory.update,
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
AuditLogAction.kick: None,
AuditLogAction.member_prune: None,
AuditLogAction.ban: None,
AuditLogAction.unban: None,
AuditLogAction.member_update: AuditLogActionCategory.update,
AuditLogAction.member_role_update: AuditLogActionCategory.update,
AuditLogAction.member_move: None,
AuditLogAction.member_disconnect: None,
AuditLogAction.bot_add: None,
AuditLogAction.role_create: AuditLogActionCategory.create,
AuditLogAction.role_update: AuditLogActionCategory.update,
AuditLogAction.role_delete: AuditLogActionCategory.delete,
AuditLogAction.invite_create: AuditLogActionCategory.create,
AuditLogAction.invite_update: AuditLogActionCategory.update,
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
AuditLogAction.webhook_create: AuditLogActionCategory.create,
AuditLogAction.webhook_update: AuditLogActionCategory.update,
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
AuditLogAction.emoji_create: AuditLogActionCategory.create,
AuditLogAction.emoji_update: AuditLogActionCategory.update,
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
AuditLogAction.message_delete: AuditLogActionCategory.delete,
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
AuditLogAction.message_pin: None,
AuditLogAction.message_unpin: None,
AuditLogAction.integration_create: AuditLogActionCategory.create,
AuditLogAction.integration_update: AuditLogActionCategory.update,
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
AuditLogAction.sticker_create: AuditLogActionCategory.create,
AuditLogAction.sticker_update: AuditLogActionCategory.update,
AuditLogAction.sticker_delete: AuditLogActionCategory.delete,
AuditLogAction.scheduled_event_create: AuditLogActionCategory.create,
AuditLogAction.scheduled_event_update: AuditLogActionCategory.update,
AuditLogAction.scheduled_event_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_create: AuditLogActionCategory.create,
AuditLogAction.thread_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_update: AuditLogActionCategory.update,
AuditLogAction.app_command_permission_update: AuditLogActionCategory.update,
AuditLogAction.automod_rule_create: AuditLogActionCategory.create,
AuditLogAction.automod_rule_update: AuditLogActionCategory.update,
AuditLogAction.automod_rule_delete: AuditLogActionCategory.delete,
AuditLogAction.automod_block_message: None,
AuditLogAction.automod_flag_message: None,
AuditLogAction.automod_timeout_member: None,
AuditLogAction.onboarding_question_create: AuditLogActionCategory.create,
AuditLogAction.onboarding_question_update: AuditLogActionCategory.update,
AuditLogAction.onboarding_update: AuditLogActionCategory.update,
AuditLogAction.guild_update: AuditLogActionCategory.update,
AuditLogAction.channel_create: AuditLogActionCategory.create,
AuditLogAction.channel_update: AuditLogActionCategory.update,
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
AuditLogAction.kick: None,
AuditLogAction.member_prune: None,
AuditLogAction.ban: None,
AuditLogAction.unban: None,
AuditLogAction.member_update: AuditLogActionCategory.update,
AuditLogAction.member_role_update: AuditLogActionCategory.update,
AuditLogAction.member_move: None,
AuditLogAction.member_disconnect: None,
AuditLogAction.bot_add: None,
AuditLogAction.role_create: AuditLogActionCategory.create,
AuditLogAction.role_update: AuditLogActionCategory.update,
AuditLogAction.role_delete: AuditLogActionCategory.delete,
AuditLogAction.invite_create: AuditLogActionCategory.create,
AuditLogAction.invite_update: AuditLogActionCategory.update,
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
AuditLogAction.webhook_create: AuditLogActionCategory.create,
AuditLogAction.webhook_update: AuditLogActionCategory.update,
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
AuditLogAction.emoji_create: AuditLogActionCategory.create,
AuditLogAction.emoji_update: AuditLogActionCategory.update,
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
AuditLogAction.message_delete: AuditLogActionCategory.delete,
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
AuditLogAction.message_pin: None,
AuditLogAction.message_unpin: None,
AuditLogAction.integration_create: AuditLogActionCategory.create,
AuditLogAction.integration_update: AuditLogActionCategory.update,
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
AuditLogAction.sticker_create: AuditLogActionCategory.create,
AuditLogAction.sticker_update: AuditLogActionCategory.update,
AuditLogAction.sticker_delete: AuditLogActionCategory.delete,
AuditLogAction.scheduled_event_create: AuditLogActionCategory.create,
AuditLogAction.scheduled_event_update: AuditLogActionCategory.update,
AuditLogAction.scheduled_event_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_create: AuditLogActionCategory.create,
AuditLogAction.thread_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_update: AuditLogActionCategory.update,
AuditLogAction.app_command_permission_update: AuditLogActionCategory.update,
AuditLogAction.automod_rule_create: AuditLogActionCategory.create,
AuditLogAction.automod_rule_update: AuditLogActionCategory.update,
AuditLogAction.automod_rule_delete: AuditLogActionCategory.delete,
AuditLogAction.automod_block_message: None,
AuditLogAction.automod_flag_message: None,
AuditLogAction.automod_timeout_member: None,
AuditLogAction.creator_monetization_request_created: None,
AuditLogAction.creator_monetization_terms_accepted: None,
AuditLogAction.onboarding_question_create: AuditLogActionCategory.create,
AuditLogAction.onboarding_question_update: AuditLogActionCategory.update,
AuditLogAction.onboarding_update: AuditLogActionCategory.update,
}
# fmt: on
return lookup[self]
@ -481,6 +492,8 @@ class AuditLogAction(Enum):
return 'auto_moderation'
elif v < 146:
return 'user'
elif v < 152:
return 'creator_monetization'
elif v < 165:
return 'onboarding_question'
elif v < 168:
@ -528,6 +541,12 @@ class TeamMembershipState(Enum):
accepted = 2
class TeamMemberRole(Enum):
admin = 'admin'
developer = 'developer'
read_only = 'read_only'
class WebhookType(Enum):
incoming = 1
channel_follower = 2
@ -590,6 +609,7 @@ class InteractionResponseType(Enum):
message_update = 7 # for components
autocomplete_result = 8
modal = 9 # for modals
premium_required = 10
class VideoQualityMode(Enum):
@ -749,16 +769,19 @@ class AutoModRuleTriggerType(Enum):
spam = 3
keyword_preset = 4
mention_spam = 5
member_profile = 6
class AutoModRuleEventType(Enum):
message_send = 1
member_update = 2
class AutoModRuleActionType(Enum):
block_message = 1
send_alert_message = 2
timeout = 3
block_member_interactions = 4
class ForumLayoutType(Enum):
@ -772,6 +795,26 @@ class ForumOrderType(Enum):
creation_date = 1
class SelectDefaultValueType(Enum):
user = 'user'
role = 'role'
channel = 'channel'
class SKUType(Enum):
subscription = 5
subscription_group = 6
class EntitlementType(Enum):
application_subscription = 8
class EntitlementOwnerType(Enum):
guild = 1
user = 2
class OnboardingPromptType(Enum):
multiple_choice = 0
dropdown = 1

14
discord/ext/commands/bot.py

@ -499,6 +499,12 @@ class BotBase(GroupMixin[None]):
``user`` parameter is now positional-only.
.. versionchanged:: 2.4
This function now respects the team member roles if the bot is team-owned.
In order to be considered an owner, they must be either an admin or
a developer.
Parameters
-----------
user: :class:`.abc.User`
@ -516,9 +522,13 @@ class BotBase(GroupMixin[None]):
return user.id in self.owner_ids
else:
app = await self.application_info() # type: ignore
app: discord.AppInfo = await self.application_info() # type: ignore
if app.team:
self.owner_ids = ids = {m.id for m in app.team.members}
self.owner_ids = ids = {
m.id
for m in app.team.members
if m.role in (discord.TeamMemberRole.admin, discord.TeamMemberRole.developer)
}
return user.id in ids
else:
self.owner_id = owner_id = app.owner.id

24
discord/ext/commands/cog.py

@ -25,6 +25,7 @@ from __future__ import annotations
import inspect
import discord
import logging
from discord import app_commands
from discord.utils import maybe_coroutine, _to_kebab_case
@ -65,6 +66,7 @@ __all__ = (
FuncT = TypeVar('FuncT', bound=Callable[..., Any])
MISSING: Any = discord.utils.MISSING
_log = logging.getLogger(__name__)
class CogMeta(type):
@ -305,6 +307,7 @@ class Cog(metaclass=CogMeta):
# Register the application commands
children: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = []
app_command_refs: Dict[str, Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = {}
if cls.__cog_is_app_commands_group__:
group = app_commands.Group(
@ -331,6 +334,16 @@ class Cog(metaclass=CogMeta):
# Get the latest parent reference
parent = lookup[parent.qualified_name] # type: ignore
# Hybrid commands already deal with updating the reference
# Due to the copy below, so we need to handle them specially
if hasattr(parent, '__commands_is_hybrid__') and hasattr(command, '__commands_is_hybrid__'):
current: Optional[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = getattr(
command, 'app_command', None
)
updated = app_command_refs.get(command.qualified_name)
if current and updated:
command.app_command = updated # type: ignore # Safe attribute access
# Update our parent's reference to our self
parent.remove_command(command.name) # type: ignore
parent.add_command(command) # type: ignore
@ -345,6 +358,13 @@ class Cog(metaclass=CogMeta):
# The type checker does not see the app_command attribute even though it exists
command.app_command = app_command # type: ignore
# Update all the references to point to the new copy
if isinstance(app_command, app_commands.Group):
for child in app_command.walk_commands():
app_command_refs[child.qualified_name] = child
if hasattr(child, '__commands_is_hybrid_app_command__') and child.qualified_name in lookup:
child.wrapped = lookup[child.qualified_name] # type: ignore
if self.__cog_app_commands_group__:
children.append(app_command) # type: ignore # Somehow it thinks it can be None here
@ -377,7 +397,7 @@ class Cog(metaclass=CogMeta):
if len(mapping) > 25:
raise TypeError('maximum number of application command children exceeded')
self.__cog_app_commands_group__._children = mapping # type: ignore # Variance issue
self.__cog_app_commands_group__._children = mapping
return self
@ -753,7 +773,7 @@ class Cog(metaclass=CogMeta):
try:
await maybe_coroutine(self.cog_unload)
except Exception:
pass
_log.exception('Ignoring exception in cog unload for Cog %r (%r)', cls, self.qualified_name)
class GroupCog(Cog):

2
discord/ext/commands/context.py

@ -251,7 +251,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
if command is None:
raise ValueError('interaction does not have command data')
bot: BotT = interaction.client # type: ignore
bot: BotT = interaction.client
data: ApplicationCommandInteractionData = interaction.data # type: ignore
if interaction.message is None:
synthetic_payload = {

6
discord/ext/commands/core.py

@ -776,7 +776,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
command = self
# command.parent is type-hinted as GroupMixin some attributes are resolved via MRO
while command.parent is not None: # type: ignore
command = command.parent # type: ignore
command = command.parent
entries.append(command.name) # type: ignore
return ' '.join(reversed(entries))
@ -794,7 +794,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
entries = []
command = self
while command.parent is not None: # type: ignore
command = command.parent # type: ignore
command = command.parent
entries.append(command)
return entries
@ -2004,7 +2004,7 @@ def check_any(*checks: Check[ContextT]) -> Check[ContextT]:
# if we're here, all checks failed
raise CheckAnyFailure(unwrapped, errors)
return check(predicate) # type: ignore
return check(predicate)
def has_role(item: Union[int, str], /) -> Check[Any]:

39
discord/ext/commands/errors.py

@ -27,6 +27,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
from discord.errors import ClientException, DiscordException
from discord.utils import _human_join
if TYPE_CHECKING:
from discord.abc import GuildChannel
@ -758,12 +759,7 @@ class MissingAnyRole(CheckFailure):
self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
fmt = _human_join(missing)
message = f'You are missing at least one of the required roles: {fmt}'
super().__init__(message)
@ -788,12 +784,7 @@ class BotMissingAnyRole(CheckFailure):
self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
fmt = _human_join(missing)
message = f'Bot is missing at least one of the required roles: {fmt}'
super().__init__(message)
@ -832,11 +823,7 @@ class MissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
if len(missing) > 2:
fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
fmt = _human_join(missing, final='and')
message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args)
@ -857,11 +844,7 @@ class BotMissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
if len(missing) > 2:
fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
fmt = _human_join(missing, final='and')
message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args)
@ -896,11 +879,7 @@ class BadUnionArgument(UserInputError):
return x.__class__.__name__
to_string = [_get_name(x) for x in converters]
if len(to_string) > 2:
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
else:
fmt = ' or '.join(to_string)
fmt = _human_join(to_string)
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into {fmt}.')
@ -933,11 +912,7 @@ class BadLiteralArgument(UserInputError):
self.argument: str = argument
to_string = [repr(l) for l in literals]
if len(to_string) > 2:
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
else:
fmt = ' or '.join(to_string)
fmt = _human_join(to_string)
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into the literal {fmt}.')

4
discord/ext/commands/flags.py

@ -485,7 +485,7 @@ class FlagConverter(metaclass=FlagsMeta):
for flag in flags.values():
if callable(flag.default):
# Type checker does not understand that flag.default is a Callable
default = await maybe_coroutine(flag.default, ctx) # type: ignore
default = await maybe_coroutine(flag.default, ctx)
setattr(self, flag.attribute, default)
else:
setattr(self, flag.attribute, flag.default)
@ -600,7 +600,7 @@ class FlagConverter(metaclass=FlagsMeta):
else:
if callable(flag.default):
# Type checker does not understand flag.default is a Callable
default = await maybe_coroutine(flag.default, ctx) # type: ignore
default = await maybe_coroutine(flag.default, ctx)
setattr(self, flag.attribute, default)
else:
setattr(self, flag.attribute, flag.default)

28
discord/ext/commands/hybrid.py

@ -297,14 +297,20 @@ def replace_parameters(
class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
def __init__(self, wrapped: Union[HybridCommand[CogT, ..., T], HybridGroup[CogT, ..., T]]) -> None:
__commands_is_hybrid_app_command__: ClassVar[bool] = True
def __init__(
self,
wrapped: Union[HybridCommand[CogT, ..., T], HybridGroup[CogT, ..., T]],
name: Optional[Union[str, app_commands.locale_str]] = None,
) -> None:
signature = inspect.signature(wrapped.callback)
params = replace_parameters(wrapped.params, wrapped.callback, signature)
wrapped.callback.__signature__ = signature.replace(parameters=params)
nsfw = getattr(wrapped.callback, '__discord_app_commands_is_nsfw__', False)
try:
super().__init__(
name=wrapped._locale_name or wrapped.name,
name=name or wrapped._locale_name or wrapped.name,
callback=wrapped.callback, # type: ignore # Signature doesn't match but we're overriding the invoke
description=wrapped._locale_description or wrapped.description or wrapped.short_doc or '',
nsfw=nsfw,
@ -398,7 +404,7 @@ class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
if self.binding is not None:
try:
# Type checker does not like runtime attribute retrieval
check: AppCommandCheck = self.binding.interaction_check # type: ignore
check: AppCommandCheck = self.binding.interaction_check
except AttributeError:
pass
else:
@ -594,6 +600,8 @@ class HybridGroup(Group[CogT, P, T]):
application command groups cannot be invoked, this creates a subcommand within
the group that can be invoked with the given group callback. If ``None``
then no fallback command is given. Defaults to ``None``.
fallback_locale: Optional[:class:`~discord.app_commands.locale_str`]
The fallback command name's locale string, if available.
"""
__commands_is_hybrid__: ClassVar[bool] = True
@ -603,7 +611,7 @@ class HybridGroup(Group[CogT, P, T]):
*args: Any,
name: Union[str, app_commands.locale_str] = MISSING,
description: Union[str, app_commands.locale_str] = MISSING,
fallback: Optional[str] = None,
fallback: Optional[Union[str, app_commands.locale_str]] = None,
**attrs: Any,
) -> None:
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None)
@ -631,7 +639,12 @@ class HybridGroup(Group[CogT, P, T]):
# However, Python does not have conditional typing so it's very hard to
# make this type depend on the with_app_command bool without a lot of needless repetition
self.app_command: app_commands.Group = MISSING
fallback, fallback_locale = (
(fallback.message, fallback) if isinstance(fallback, app_commands.locale_str) else (fallback, None)
)
self.fallback: Optional[str] = fallback
self.fallback_locale: Optional[app_commands.locale_str] = fallback_locale
if self.with_app_command:
guild_ids = attrs.pop('guild_ids', None) or getattr(
@ -654,8 +667,7 @@ class HybridGroup(Group[CogT, P, T]):
self.app_command.module = self.module
if fallback is not None:
command = HybridAppCommand(self)
command.name = fallback
command = HybridAppCommand(self, name=fallback_locale or fallback)
self.app_command.add_command(command)
@property
@ -920,9 +932,9 @@ def hybrid_group(
If the function is not a coroutine or is already a command.
"""
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
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 decorator # type: ignore
return decorator

11
discord/ext/commands/parameters.py

@ -175,7 +175,13 @@ class Parameter(inspect.Parameter):
if self._displayed_default is not empty:
return self._displayed_default
return None if self.required else str(self.default)
if self.required:
return None
if callable(self.default) or self.default is None:
return None
return str(self.default)
@property
def displayed_name(self) -> Optional[str]:
@ -197,7 +203,7 @@ class Parameter(inspect.Parameter):
"""
# pre-condition: required is False
if callable(self.default):
return await maybe_coroutine(self.default, ctx) # type: ignore
return await maybe_coroutine(self.default, ctx)
return self.default
@ -300,6 +306,7 @@ CurrentGuild = parameter(
displayed_default='<this server>',
converter=GuildConverter,
)
CurrentGuild._fallback = True
class Signature(inspect.Signature):

241
discord/flags.py

@ -26,7 +26,21 @@ from __future__ import annotations
from functools import reduce
from operator import or_
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Iterator, List, Optional, Tuple, Type, TypeVar, overload
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
Iterator,
List,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
overload,
)
from .enums import UserFlags
@ -44,6 +58,9 @@ __all__ = (
'ChannelFlags',
'AutoModPresets',
'MemberFlags',
'AttachmentFlags',
'RoleFlags',
'SKUFlags',
)
BF = TypeVar('BF', bound='BaseFlags')
@ -1195,7 +1212,7 @@ class Intents(BaseFlags):
"""
return 1 << 16
@flag_value
@alias_flag_value
def auto_moderation(self):
""":class:`bool`: Whether auto moderation related events are enabled.
@ -1618,10 +1635,19 @@ class ChannelFlags(BaseFlags):
"""
return 1 << 4
@flag_value
def hide_media_download_options(self):
""":class:`bool`: Returns ``True`` if the client hides embedded media download options in a :class:`ForumChannel`.
Only available in media channels.
.. versionadded:: 2.4
"""
return 1 << 15
class ArrayFlags(BaseFlags):
@classmethod
def _from_value(cls: Type[Self], value: List[int]) -> Self:
def _from_value(cls: Type[Self], value: Sequence[int]) -> Self:
self = cls.__new__(cls)
# This is a micro-optimization given the frequency this object can be created.
# (1).__lshift__ is used in place of lambda x: 1 << x
@ -1810,3 +1836,212 @@ class MemberFlags(BaseFlags):
def started_onboarding(self):
""":class:`bool`: Returns ``True`` if the member has started onboarding."""
return 1 << 3
@fill_with_flags()
class AttachmentFlags(BaseFlags):
r"""Wraps up the Discord Attachment flags
.. versionadded:: 2.4
.. container:: operations
.. describe:: x == y
Checks if two AttachmentFlags are equal.
.. describe:: x != y
Checks if two AttachmentFlags are not equal.
.. describe:: x | y, x |= y
Returns a AttachmentFlags instance with all enabled flags from
both x and y.
.. describe:: x & y, x &= y
Returns a AttachmentFlags instance with only flags enabled on
both x and y.
.. describe:: x ^ y, x ^= y
Returns a AttachmentFlags instance with only flags enabled on
only one of x or y, not on both.
.. describe:: ~x
Returns a AttachmentFlags instance with all flags inverted from x.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
-----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def clip(self):
""":class:`bool`: Returns ``True`` if the attachment is a clip."""
return 1 << 0
@flag_value
def thumbnail(self):
""":class:`bool`: Returns ``True`` if the attachment is a thumbnail."""
return 1 << 1
@flag_value
def remix(self):
""":class:`bool`: Returns ``True`` if the attachment has been edited using the remix feature."""
return 1 << 2
@fill_with_flags()
class RoleFlags(BaseFlags):
r"""Wraps up the Discord Role flags
.. versionadded:: 2.4
.. container:: operations
.. describe:: x == y
Checks if two RoleFlags are equal.
.. describe:: x != y
Checks if two RoleFlags are not equal.
.. describe:: x | y, x |= y
Returns a RoleFlags instance with all enabled flags from
both x and y.
.. describe:: x & y, x &= y
Returns a RoleFlags instance with only flags enabled on
both x and y.
.. describe:: x ^ y, x ^= y
Returns a RoleFlags instance with only flags enabled on
only one of x or y, not on both.
.. describe:: ~x
Returns a RoleFlags instance with all flags inverted from x.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
-----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def in_prompt(self):
""":class:`bool`: Returns ``True`` if the role can be selected by members in an onboarding prompt."""
return 1 << 0
@fill_with_flags()
class SKUFlags(BaseFlags):
r"""Wraps up the Discord SKU flags
.. versionadded:: 2.4
.. container:: operations
.. describe:: x == y
Checks if two SKUFlags are equal.
.. describe:: x != y
Checks if two SKUFlags are not equal.
.. describe:: x | y, x |= y
Returns a SKUFlags instance with all enabled flags from
both x and y.
.. describe:: x & y, x &= y
Returns a SKUFlags instance with only flags enabled on
both x and y.
.. describe:: x ^ y, x ^= y
Returns a SKUFlags instance with only flags enabled on
only one of x or y, not on both.
.. describe:: ~x
Returns a SKUFlags instance with all flags inverted from x.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
-----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def available(self):
""":class:`bool`: Returns ``True`` if the SKU is available for purchase."""
return 1 << 2
@flag_value
def guild_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a guild subscription."""
return 1 << 7
@flag_value
def user_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a user subscription."""
return 1 << 8

86
discord/gateway.py

@ -34,7 +34,7 @@ import threading
import traceback
import zlib
from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar
from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar, Tuple
import aiohttp
import yarl
@ -59,7 +59,7 @@ if TYPE_CHECKING:
from .client import Client
from .state import ConnectionState
from .voice_client import VoiceClient
from .voice_state import VoiceConnectionState
class ReconnectWebSocket(Exception):
@ -132,11 +132,12 @@ class KeepAliveHandler(threading.Thread):
shard_id: Optional[int] = None,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
daemon: bool = kwargs.pop('daemon', True)
name: str = kwargs.pop('name', f'keep-alive-handler:shard-{shard_id}')
super().__init__(*args, daemon=daemon, name=name, **kwargs)
self.ws: DiscordWebSocket = ws
self._main_thread_id: int = ws.thread_id
self.interval: Optional[float] = interval
self.daemon: bool = True
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.'
@ -212,7 +213,8 @@ class KeepAliveHandler(threading.Thread):
class VoiceKeepAliveHandler(KeepAliveHandler):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}')
super().__init__(*args, name=name, **kwargs)
self.recent_ack_latencies: Deque[float] = deque(maxlen=20)
self.msg: str = 'Keeping shard ID %s voice websocket alive with timestamp %s.'
self.block_msg: str = 'Shard ID %s voice heartbeat blocked for more than %s seconds'
@ -622,8 +624,8 @@ class DiscordWebSocket:
elif msg.type is aiohttp.WSMsgType.BINARY:
await self.received_message(msg.data)
elif msg.type is aiohttp.WSMsgType.ERROR:
_log.debug('Received %s', msg)
raise msg.data
_log.debug('Received error %s', msg)
raise WebSocketClosure
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSE):
_log.debug('Received %s', msg)
raise WebSocketClosure
@ -795,7 +797,7 @@ class DiscordVoiceWebSocket:
if TYPE_CHECKING:
thread_id: int
_connection: VoiceClient
_connection: VoiceConnectionState
gateway: str
_max_heartbeat_timeout: float
@ -864,16 +866,21 @@ class DiscordVoiceWebSocket:
await self.send_as_json(payload)
@classmethod
async def from_client(
cls, client: VoiceClient, *, resume: bool = False, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None
async def from_connection_state(
cls,
state: VoiceConnectionState,
*,
resume: bool = False,
hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None,
) -> Self:
"""Creates a voice websocket for the :class:`VoiceClient`."""
gateway = 'wss://' + client.endpoint + '/?v=4'
gateway = f'wss://{state.endpoint}/?v=4'
client = state.voice_client
http = client._state.http
socket = await http.ws_connect(gateway, compress=15)
ws = cls(socket, loop=client.loop, hook=hook)
ws.gateway = gateway
ws._connection = client
ws._connection = state
ws._max_heartbeat_timeout = 60.0
ws.thread_id = threading.get_ident()
@ -949,29 +956,49 @@ class DiscordVoiceWebSocket:
state.voice_port = data['port']
state.endpoint_ip = data['ip']
_log.debug('Connecting to voice socket')
await self.loop.sock_connect(state.socket, (state.endpoint_ip, state.voice_port))
state.ip, state.port = await self.discover_ip()
# there *should* always be at least one supported mode (xsalsa20_poly1305)
modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes]
_log.debug('received supported encryption modes: %s', ', '.join(modes))
mode = modes[0]
await self.select_protocol(state.ip, state.port, mode)
_log.debug('selected the voice protocol for use (%s)', mode)
async def discover_ip(self) -> Tuple[str, int]:
state = self._connection
packet = bytearray(74)
struct.pack_into('>H', packet, 0, 1) # 1 = Send
struct.pack_into('>H', packet, 2, 70) # 70 = Length
struct.pack_into('>I', packet, 4, state.ssrc)
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
recv = await self.loop.sock_recv(state.socket, 74)
_log.debug('received packet in initial_connection: %s', recv)
_log.debug('Sending ip discovery packet')
await self.loop.sock_sendall(state.socket, packet)
fut: asyncio.Future[bytes] = self.loop.create_future()
def get_ip_packet(data: bytes):
if data[1] == 0x02 and len(data) == 74:
self.loop.call_soon_threadsafe(fut.set_result, data)
fut.add_done_callback(lambda f: state.remove_socket_listener(get_ip_packet))
state.add_socket_listener(get_ip_packet)
recv = await fut
_log.debug('Received ip discovery packet: %s', recv)
# the ip is ascii starting at the 8th byte and ending at the first null
ip_start = 8
ip_end = recv.index(0, ip_start)
state.ip = recv[ip_start:ip_end].decode('ascii')
ip = recv[ip_start:ip_end].decode('ascii')
state.port = struct.unpack_from('>H', recv, len(recv) - 2)[0]
_log.debug('detected ip: %s port: %s', state.ip, state.port)
port = struct.unpack_from('>H', recv, len(recv) - 2)[0]
_log.debug('detected ip: %s port: %s', ip, port)
# there *should* always be at least one supported mode (xsalsa20_poly1305)
modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes]
_log.debug('received supported encryption modes: %s', ", ".join(modes))
mode = modes[0]
await self.select_protocol(state.ip, state.port, mode)
_log.debug('selected the voice protocol for use (%s)', mode)
return ip, port
@property
def latency(self) -> float:
@ -993,9 +1020,8 @@ class DiscordVoiceWebSocket:
self.secret_key = self._connection.secret_key = data['secret_key']
# Send a speak command with the "not speaking" state.
# This also tells Discord our SSRC value, which Discord requires
# before sending any voice data (and is the real reason why we
# call this here).
# This also tells Discord our SSRC value, which Discord requires before
# sending any voice data (and is the real reason why we call this here).
await self.speak(SpeakingState.none)
async def poll_event(self) -> None:
@ -1004,10 +1030,10 @@ class DiscordVoiceWebSocket:
if msg.type is aiohttp.WSMsgType.TEXT:
await self.received_message(utils._from_json(msg.data))
elif msg.type is aiohttp.WSMsgType.ERROR:
_log.debug('Received %s', msg)
_log.debug('Received voice %s', msg)
raise ConnectionClosed(self.ws, shard_id=None) from msg.data
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING):
_log.debug('Received %s', msg)
_log.debug('Received voice %s', msg)
raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code)
async def close(self, code: int = 1000) -> None:

35
discord/guild.py

@ -134,6 +134,7 @@ if TYPE_CHECKING:
from .types.integration import IntegrationType
from .types.snowflake import SnowflakeList
from .types.widget import EditWidgetSettings
from .types.audit_log import AuditLogEvent
from .message import EmojiInputType
VocalGuildChannel = Union[VoiceChannel, StageChannel]
@ -473,9 +474,15 @@ class Guild(Hashable):
role = Role(guild=self, data=r, state=state)
self._roles[role.id] = role
self.emojis: Tuple[Emoji, ...] = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
self.stickers: Tuple[GuildSticker, ...] = tuple(
map(lambda d: state.store_sticker(self, d), guild.get('stickers', []))
self.emojis: Tuple[Emoji, ...] = (
tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
if state.cache_guild_expressions
else ()
)
self.stickers: Tuple[GuildSticker, ...] = (
tuple(map(lambda d: state.store_sticker(self, d), guild.get('stickers', [])))
if state.cache_guild_expressions
else ()
)
self.features: List[GuildFeature] = guild.get('features', [])
self._splash: Optional[str] = guild.get('splash')
@ -2899,7 +2906,10 @@ class Guild(Hashable):
payload['tags'] = emoji
data = await self._state.http.create_guild_sticker(self.id, payload, file, reason)
return self._state.store_sticker(self, data)
if self._state.cache_guild_expressions:
return self._state.store_sticker(self, data)
else:
return GuildSticker(state=self._state, data=data)
async def delete_sticker(self, sticker: Snowflake, /, *, reason: Optional[str] = None) -> None:
"""|coro|
@ -3308,7 +3318,10 @@ class Guild(Hashable):
role_ids = []
data = await self._state.http.create_custom_emoji(self.id, name, img, roles=role_ids, reason=reason)
return self._state.store_emoji(self, data)
if self._state.cache_guild_expressions:
return self._state.store_emoji(self, data)
else:
return Emoji(guild=self, state=self._state, data=data)
async def delete_emoji(self, emoji: Snowflake, /, *, reason: Optional[str] = None) -> None:
"""|coro|
@ -3596,7 +3609,7 @@ class Guild(Hashable):
The guild must have ``COMMUNITY`` in :attr:`~Guild.features`.
You must have :attr:`~Permissions.manage_guild` to do this.as well.
You must have :attr:`~Permissions.manage_guild` to do this as well.
.. versionadded:: 2.0
@ -3855,7 +3868,7 @@ class Guild(Hashable):
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
before_id = before.id if before else None
data = await self._state.http.get_audit_logs(
self.id, limit=retrieve, user_id=user_id, action_type=action, before=before_id
self.id, limit=retrieve, user_id=user_id, action_type=action_type, before=before_id
)
entries = data.get('audit_log_entries', [])
@ -3871,7 +3884,7 @@ class Guild(Hashable):
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
after_id = after.id if after else None
data = await self._state.http.get_audit_logs(
self.id, limit=retrieve, user_id=user_id, action_type=action, after=after_id
self.id, limit=retrieve, user_id=user_id, action_type=action_type, after=after_id
)
entries = data.get('audit_log_entries', [])
@ -3889,8 +3902,10 @@ class Guild(Hashable):
else:
user_id = None
if action:
action = action.value
if action is not MISSING:
action_type: Optional[AuditLogEvent] = action.value
else:
action_type = None
if isinstance(before, datetime.datetime):
before = Object(id=utils.time_snowflake(before, high=False))

84
discord/http.py

@ -68,7 +68,6 @@ if TYPE_CHECKING:
from .embeds import Embed
from .message import Attachment
from .flags import MessageFlags
from .enums import AuditLogAction
from .types import (
appinfo,
@ -92,6 +91,7 @@ if TYPE_CHECKING:
scheduled_event,
sticker,
welcome_screen,
sku,
)
from .types.snowflake import Snowflake, SnowflakeList
@ -1728,7 +1728,7 @@ class HTTPClient:
before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None,
user_id: Optional[Snowflake] = None,
action_type: Optional[AuditLogAction] = None,
action_type: Optional[audit_log.AuditLogEvent] = None,
) -> Response[audit_log.AuditLog]:
params: Dict[str, Any] = {'limit': limit}
if before:
@ -1920,6 +1920,7 @@ class HTTPClient:
'topic',
'privacy_level',
'send_start_notification',
'guild_scheduled_event_id',
)
payload = {k: v for k, v in payload.items() if k in valid_keys}
@ -2376,10 +2377,87 @@ class HTTPClient:
reason=reason,
)
# SKU
def get_skus(self, application_id: Snowflake) -> Response[List[sku.SKU]]:
return self.request(Route('GET', '/applications/{application_id}/skus', application_id=application_id))
def get_entitlements(
self,
application_id: Snowflake,
user_id: Optional[Snowflake] = None,
sku_ids: Optional[SnowflakeList] = None,
before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None,
limit: Optional[int] = None,
guild_id: Optional[Snowflake] = None,
exclude_ended: Optional[bool] = None,
) -> Response[List[sku.Entitlement]]:
params: Dict[str, Any] = {}
if user_id is not None:
params['user_id'] = user_id
if sku_ids is not None:
params['sku_ids'] = ','.join(map(str, sku_ids))
if before is not None:
params['before'] = before
if after is not None:
params['after'] = after
if limit is not None:
params['limit'] = limit
if guild_id is not None:
params['guild_id'] = guild_id
if exclude_ended is not None:
params['exclude_ended'] = int(exclude_ended)
return self.request(
Route('GET', '/applications/{application_id}/entitlements', application_id=application_id), params=params
)
def get_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) -> Response[sku.Entitlement]:
return self.request(
Route(
'GET',
'/applications/{application_id}/entitlements/{entitlement_id}',
application_id=application_id,
entitlement_id=entitlement_id,
),
)
def create_entitlement(
self, application_id: Snowflake, sku_id: Snowflake, owner_id: Snowflake, owner_type: sku.EntitlementOwnerType
) -> Response[sku.Entitlement]:
payload = {
'sku_id': sku_id,
'owner_id': owner_id,
'owner_type': owner_type,
}
return self.request(
Route(
'POST',
'/applications/{application.id}/entitlements',
application_id=application_id,
),
json=payload,
)
def delete_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) -> Response[None]:
return self.request(
Route(
'DELETE',
'/applications/{application_id}/entitlements/{entitlement_id}',
application_id=application_id,
entitlement_id=entitlement_id,
),
)
# 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 modify_guild_onboarding(
def edit_guild_onboarding(
self,
guild_id: Snowflake,
*,

48
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
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List
import asyncio
import datetime
@ -37,6 +37,7 @@ from .errors import InteractionResponded, HTTPException, ClientException, Discor
from .flags import MessageFlags
from .channel import ChannelType
from ._types import ClientT
from .sku import Entitlement
from .user import User
from .member import Member
@ -110,6 +111,10 @@ class Interaction(Generic[ClientT]):
The channel the interaction was sent from.
Note that due to a Discord limitation, if sent from a DM channel :attr:`~DMChannel.recipient` is ``None``.
entitlement_sku_ids: List[:class:`int`]
The entitlement SKU IDs that the user has.
entitlements: List[:class:`Entitlement`]
The entitlements that the guild or user has.
application_id: :class:`int`
The application ID that the interaction was for.
user: Union[:class:`User`, :class:`Member`]
@ -150,6 +155,8 @@ class Interaction(Generic[ClientT]):
'guild_locale',
'extras',
'command_failed',
'entitlement_sku_ids',
'entitlements',
'_permissions',
'_app_permissions',
'_state',
@ -185,6 +192,8 @@ class Interaction(Generic[ClientT]):
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
self.channel: Optional[InteractionChannel] = None
self.application_id: int = int(data['application_id'])
self.entitlement_sku_ids: List[int] = [int(x) for x in data.get('entitlement_skus', []) or []]
self.entitlements: List[Entitlement] = [Entitlement(self._state, x) for x in data.get('entitlements', [])]
self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
self.guild_locale: Optional[Locale]
@ -254,7 +263,10 @@ class Interaction(Generic[ClientT]):
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
return self._state and self._state._get_guild(self.guild_id)
# The user.guild attribute is set in __init__ to the fallback guild if available
# Therefore, we can use that instead of recreating it every time this property is
# accessed
return (self._state and self._state._get_guild(self.guild_id)) or getattr(self.user, 'guild', None)
@property
def channel_id(self) -> Optional[int]:
@ -981,6 +993,38 @@ class InteractionResponse(Generic[ClientT]):
self._parent._state.store_view(modal)
self._response_type = InteractionResponseType.modal
async def require_premium(self) -> None:
"""|coro|
Sends a message to the user prompting them that a premium purchase is required for this interaction.
This type of response is only available for applications that have a premium SKU set up.
Raises
-------
HTTPException
Sending the response failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self._response_type:
raise InteractionResponded(self._parent)
parent = self._parent
adapter = async_context.get()
http = parent._state.http
params = interaction_response_params(InteractionResponseType.premium_required.value)
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
proxy=http.proxy,
proxy_auth=http.proxy_auth,
params=params,
)
self._response_type = InteractionResponseType.premium_required
async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None:
"""|coro|

3
discord/invite.py

@ -47,6 +47,7 @@ if TYPE_CHECKING:
InviteGuild as InviteGuildPayload,
GatewayInvite as GatewayInvitePayload,
)
from .types.guild import GuildFeature
from .types.channel import (
PartialChannel as InviteChannelPayload,
)
@ -189,7 +190,7 @@ class PartialInviteGuild:
self._state: ConnectionState = state
self.id: int = id
self.name: str = data['name']
self.features: List[str] = data.get('features', [])
self.features: List[GuildFeature] = data.get('features', [])
self._icon: Optional[str] = data.get('icon')
self._banner: Optional[str] = data.get('banner')
self._splash: Optional[str] = data.get('splash')

4
discord/member.py

@ -840,7 +840,7 @@ class Member(discord.abc.Messageable, _UserTag):
Raises
-------
Forbidden
You do not have the proper permissions to the action requested.
You do not have the proper permissions to do the action requested.
HTTPException
The operation failed.
TypeError
@ -932,7 +932,7 @@ class Member(discord.abc.Messageable, _UserTag):
ClientException
You are not connected to a voice channel.
Forbidden
You do not have the proper permissions to the action requested.
You do not have the proper permissions to do the action requested.
HTTPException
The operation failed.
"""

10
discord/message.py

@ -54,7 +54,7 @@ from .errors import HTTPException
from .components import _component_factory
from .embeds import Embed
from .member import Member
from .flags import MessageFlags
from .flags import MessageFlags, AttachmentFlags
from .file import File
from .utils import escape_mentions, MISSING
from .http import handle_message_parameters
@ -207,6 +207,7 @@ class Attachment(Hashable):
'ephemeral',
'duration',
'waveform',
'_flags',
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
@ -226,6 +227,13 @@ class Attachment(Hashable):
waveform = data.get('waveform')
self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None
self._flags: int = data.get('flags', 0)
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: The attachment's flags."""
return AttachmentFlags._from_value(self._flags)
def is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith('SPOILER_')

2
discord/oggparse.py

@ -99,7 +99,7 @@ class OggStream:
elif not head:
return None
else:
raise OggError('invalid header magic')
raise OggError(f'invalid header magic {head}')
def _iter_pages(self) -> Generator[OggPage, None, None]:
page = self._next_page()

64
discord/opus.py

@ -39,10 +39,17 @@ from .errors import DiscordException
if TYPE_CHECKING:
T = TypeVar('T')
APPLICATION_CTL = Literal['audio', 'voip', 'lowdelay']
BAND_CTL = Literal['narrow', 'medium', 'wide', 'superwide', 'full']
SIGNAL_CTL = Literal['auto', 'voice', 'music']
class ApplicationCtl(TypedDict):
audio: int
voip: int
lowdelay: int
class BandCtl(TypedDict):
narrow: int
medium: int
@ -65,6 +72,8 @@ __all__ = (
_log = logging.getLogger(__name__)
OPUS_SILENCE = b'\xF8\xFF\xFE'
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
c_float_ptr = ctypes.POINTER(ctypes.c_float)
@ -90,9 +99,10 @@ OK = 0
BAD_ARG = -1
# Encoder CTLs
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
APPLICATION_LOWDELAY = 2051
APPLICATION_AUDIO = 'audio'
APPLICATION_VOIP = 'voip'
APPLICATION_LOWDELAY = 'lowdelay'
# These remain as strings for backwards compat
CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
@ -105,6 +115,12 @@ CTL_SET_GAIN = 4034
CTL_LAST_PACKET_DURATION = 4039
# fmt: on
application_ctl: ApplicationCtl = {
'audio': 2049,
'voip': 2048,
'lowdelay': 2051,
}
band_ctl: BandCtl = {
'narrow': 1101,
'medium': 1102,
@ -319,16 +335,38 @@ class _OpusStruct:
class Encoder(_OpusStruct):
def __init__(self, application: int = APPLICATION_AUDIO):
_OpusStruct.get_opus_version()
self.application: int = application
def __init__(
self,
*,
application: APPLICATION_CTL = 'audio',
bitrate: int = 128,
fec: bool = True,
expected_packet_loss: float = 0.15,
bandwidth: BAND_CTL = 'full',
signal_type: SIGNAL_CTL = 'auto',
):
if application not in application_ctl:
raise ValueError(f'{application} is not a valid application setting. Try one of: {"".join(application_ctl)}')
if not 16 <= bitrate <= 512:
raise ValueError(f'bitrate must be between 16 and 512, not {bitrate}')
if not 0 < expected_packet_loss <= 1.0:
raise ValueError(
f'expected_packet_loss must be a positive number less than or equal to 1, not {expected_packet_loss}'
)
_OpusStruct.get_opus_version() # lazy loads the opus library
self.application: int = application_ctl[application]
self._state: EncoderStruct = self._create_state()
self.set_bitrate(128)
self.set_fec(True)
self.set_expected_packet_loss_percent(0.15)
self.set_bandwidth('full')
self.set_signal_type('auto')
self.set_bitrate(bitrate)
self.set_fec(fec)
if fec:
self.set_expected_packet_loss_percent(expected_packet_loss)
self.set_bandwidth(bandwidth)
self.set_signal_type(signal_type)
def __del__(self) -> None:
if hasattr(self, '_state'):
@ -355,7 +393,7 @@ class Encoder(_OpusStruct):
def set_signal_type(self, req: SIGNAL_CTL) -> None:
if req not in signal_ctl:
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
raise KeyError(f'{req!r} is not a valid signal type setting. Try one of: {",".join(signal_ctl)}')
k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)

49
discord/permissions.py

@ -141,9 +141,12 @@ class Permissions(BaseFlags):
self.value = permissions
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid permission name.')
setattr(self, key, value)
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)
def is_subset(self, other: Permissions) -> bool:
"""Returns ``True`` if self has the same or fewer permissions as other."""
@ -183,7 +186,8 @@ class Permissions(BaseFlags):
"""A factory method that creates a :class:`Permissions` with all
permissions set to ``True``.
"""
return cls(0b11111111111111111111111111111111111111111111111)
# Some of these are 0 because we don't want to set unnecessary bits
return cls(0b0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
@classmethod
def _timeout_mask(cls) -> int:
@ -232,7 +236,7 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.3
Added :attr:`use_soundboard`, :attr:`create_expressions` permissions.
"""
return cls(0b01000111110110110011111101111111111101010001)
return cls(0b0000_0000_0000_0000_0000_0100_0111_1101_1011_0011_1111_0111_1111_1111_0101_0001)
@classmethod
def general(cls) -> Self:
@ -248,7 +252,7 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.3
Added :attr:`create_expressions` permission.
"""
return cls(0b10000000000001110000000010000000010010110000)
return cls(0b0000_0000_0000_0000_0000_1000_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000)
@classmethod
def membership(cls) -> Self:
@ -257,7 +261,7 @@ class Permissions(BaseFlags):
.. versionadded:: 1.7
"""
return cls(0b10000000000001100000000000000000000000111)
return cls(0b0000_0000_0000_0000_0000_0001_0000_0000_0000_1100_0000_0000_0000_0000_0000_0111)
@classmethod
def text(cls) -> Self:
@ -275,13 +279,13 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.3
Added :attr:`send_voice_messages` permission.
"""
return cls(0b10000000111110010000000000001111111100001000000)
return cls(0b0000_0000_0000_0000_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000)
@classmethod
def voice(cls) -> Self:
"""A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to ``True``."""
return cls(0b1001001000000000000011111100000000001100000000)
return cls(0b0000_0000_0000_0000_0010_0100_1000_0000_0000_0011_1111_0000_0000_0011_0000_0000)
@classmethod
def stage(cls) -> Self:
@ -306,7 +310,7 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.0
Added :attr:`manage_channels` permission and removed :attr:`request_to_speak` permission.
"""
return cls(0b1010000000000000000010000)
return cls(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0001_0100_0000_0000_0000_0001_0000)
@classmethod
def elevated(cls) -> Self:
@ -327,7 +331,16 @@ class Permissions(BaseFlags):
.. versionadded:: 2.0
"""
return cls(0b10000010001110000000000000010000000111110)
return cls(0b0000_0000_0000_0000_0000_0001_0000_0100_0111_0000_0000_0000_0010_0000_0011_1110)
@classmethod
def events(cls) -> Self:
"""A factory method that creates a :class:`Permissions` with all
"Events" permissions from the official Discord UI set to ``True``.
.. versionadded:: 2.4
"""
return cls(0b0000_0000_0000_0000_0001_0000_0000_0010_0000_0000_0000_0000_0000_0000_0000_0000)
@classmethod
def advanced(cls) -> Self:
@ -351,8 +364,9 @@ class Permissions(BaseFlags):
A list of key/value pairs to bulk update permissions with.
"""
for key, value in kwargs.items():
if key in self.VALID_FLAGS:
setattr(self, key, value)
flag = self.VALID_FLAGS.get(key)
if flag is not None:
self._set_flag(flag, value)
def handle_overwrite(self, allow: int, deny: int) -> None:
# Basically this is what's happening here.
@ -684,6 +698,14 @@ class Permissions(BaseFlags):
"""
return 1 << 43
@flag_value
def create_events(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create guild events.
.. versionadded:: 2.4
"""
return 1 << 44
@flag_value
def use_external_sounds(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use sounds from other guilds.
@ -819,6 +841,7 @@ class PermissionOverwrite:
use_external_sounds: Optional[bool]
send_voice_messages: Optional[bool]
create_expressions: Optional[bool]
create_events: Optional[bool]
def __init__(self, **kwargs: Optional[bool]):
self._values: Dict[str, Optional[bool]] = {}

133
discord/player.py

@ -25,6 +25,7 @@ from __future__ import annotations
import threading
import subprocess
import warnings
import audioop
import asyncio
import logging
@ -39,7 +40,7 @@ from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, T
from .enums import SpeakingState
from .errors import ClientException
from .opus import Encoder as OpusEncoder
from .opus import Encoder as OpusEncoder, OPUS_SILENCE
from .oggparse import OggStream
from .utils import MISSING
@ -145,6 +146,8 @@ class FFmpegAudio(AudioSource):
.. versionadded:: 1.3
"""
BLOCKSIZE: int = io.DEFAULT_BUFFER_SIZE
def __init__(
self,
source: Union[str, io.BufferedIOBase],
@ -153,12 +156,25 @@ class FFmpegAudio(AudioSource):
args: Any,
**subprocess_kwargs: Any,
):
piping = subprocess_kwargs.get('stdin') == subprocess.PIPE
if piping and isinstance(source, str):
piping_stdin = subprocess_kwargs.get('stdin') == subprocess.PIPE
if piping_stdin and isinstance(source, str):
raise TypeError("parameter conflict: 'source' parameter cannot be a string when piping to stdin")
stderr: Optional[IO[bytes]] = subprocess_kwargs.pop('stderr', None)
if stderr == subprocess.PIPE:
warnings.warn("Passing subprocess.PIPE does nothing", DeprecationWarning, stacklevel=3)
stderr = None
piping_stderr = False
if stderr is not None:
try:
stderr.fileno()
except Exception:
piping_stderr = True
args = [executable, *args]
kwargs = {'stdout': subprocess.PIPE}
kwargs = {'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE if piping_stderr else stderr}
kwargs.update(subprocess_kwargs)
# Ensure attribute is assigned even in the case of errors
@ -166,15 +182,24 @@ class FFmpegAudio(AudioSource):
self._process = self._spawn_process(args, **kwargs)
self._stdout: IO[bytes] = self._process.stdout # type: ignore # process stdout is explicitly set
self._stdin: Optional[IO[bytes]] = None
self._pipe_thread: Optional[threading.Thread] = None
self._stderr: Optional[IO[bytes]] = None
self._pipe_writer_thread: Optional[threading.Thread] = None
self._pipe_reader_thread: Optional[threading.Thread] = None
if piping:
n = f'popen-stdin-writer:{id(self):#x}'
if piping_stdin:
n = f'popen-stdin-writer:pid-{self._process.pid}'
self._stdin = self._process.stdin
self._pipe_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n)
self._pipe_thread.start()
self._pipe_writer_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n)
self._pipe_writer_thread.start()
if piping_stderr:
n = f'popen-stderr-reader:pid-{self._process.pid}'
self._stderr = self._process.stderr
self._pipe_reader_thread = threading.Thread(target=self._pipe_reader, args=(stderr,), daemon=True, name=n)
self._pipe_reader_thread.start()
def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen:
_log.debug('Spawning ffmpeg process with command: %s', args)
process = None
try:
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)
@ -187,7 +212,8 @@ class FFmpegAudio(AudioSource):
return process
def _kill_process(self) -> None:
proc = self._process
# this function gets called in __del__ so instance attributes might not even exist
proc = getattr(self, '_process', MISSING)
if proc is MISSING:
return
@ -207,8 +233,7 @@ class FFmpegAudio(AudioSource):
def _pipe_writer(self, source: io.BufferedIOBase) -> None:
while self._process:
# arbitrarily large read size
data = source.read(8192)
data = source.read(self.BLOCKSIZE)
if not data:
if self._stdin is not None:
self._stdin.close()
@ -222,9 +247,27 @@ class FFmpegAudio(AudioSource):
self._process.terminate()
return
def _pipe_reader(self, dest: IO[bytes]) -> None:
while self._process:
if self._stderr is None:
return
try:
data: bytes = self._stderr.read(self.BLOCKSIZE)
except Exception:
_log.debug('Read error for %s, this is probably not a problem', self, exc_info=True)
return
if data is None:
return
try:
dest.write(data)
except Exception:
_log.exception('Write error for %s', self)
self._stderr.close()
return
def cleanup(self) -> None:
self._kill_process()
self._process = self._stdout = self._stdin = MISSING
self._process = self._stdout = self._stdin = self._stderr = MISSING
class FFmpegPCMAudio(FFmpegAudio):
@ -250,7 +293,6 @@ class FFmpegPCMAudio(FFmpegAudio):
to the stdin of ffmpeg. Defaults to ``False``.
stderr: Optional[:term:`py:file object`]
A file-like object to pass to the Popen constructor.
Could also be an instance of ``subprocess.PIPE``.
before_options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`]
@ -268,7 +310,7 @@ class FFmpegPCMAudio(FFmpegAudio):
*,
executable: str = 'ffmpeg',
pipe: bool = False,
stderr: Optional[IO[str]] = None,
stderr: Optional[IO[bytes]] = None,
before_options: Optional[str] = None,
options: Optional[str] = None,
) -> None:
@ -280,7 +322,14 @@ class FFmpegPCMAudio(FFmpegAudio):
args.append('-i')
args.append('-' if pipe else source)
args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning'))
# fmt: off
args.extend(('-f', 's16le',
'-ar', '48000',
'-ac', '2',
'-loglevel', 'warning',
'-blocksize', str(self.BLOCKSIZE)))
# fmt: on
if isinstance(options, str):
args.extend(shlex.split(options))
@ -348,7 +397,6 @@ class FFmpegOpusAudio(FFmpegAudio):
to the stdin of ffmpeg. Defaults to ``False``.
stderr: Optional[:term:`py:file object`]
A file-like object to pass to the Popen constructor.
Could also be an instance of ``subprocess.PIPE``.
before_options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`]
@ -381,7 +429,7 @@ class FFmpegOpusAudio(FFmpegAudio):
args.append('-i')
args.append('-' if pipe else source)
codec = 'copy' if codec in ('opus', 'libopus') else 'libopus'
codec = 'copy' if codec in ('opus', 'libopus', 'copy') else 'libopus'
bitrate = bitrate if bitrate is not None else 128
# fmt: off
@ -391,7 +439,10 @@ class FFmpegOpusAudio(FFmpegAudio):
'-ar', '48000',
'-ac', '2',
'-b:a', f'{bitrate}k',
'-loglevel', 'warning'))
'-loglevel', 'warning',
'-fec', 'true',
'-packet_loss', '15',
'-blocksize', str(self.BLOCKSIZE)))
# fmt: on
if isinstance(options, str):
@ -643,8 +694,7 @@ class AudioPlayer(threading.Thread):
*,
after: Optional[Callable[[Optional[Exception]], Any]] = None,
) -> None:
threading.Thread.__init__(self)
self.daemon: bool = True
super().__init__(daemon=True, name=f'audio-player:{id(self):#x}')
self.source: AudioSource = source
self.client: VoiceClient = client
self.after: Optional[Callable[[Optional[Exception]], Any]] = after
@ -653,7 +703,6 @@ class AudioPlayer(threading.Thread):
self._resumed: threading.Event = threading.Event()
self._resumed.set() # we are not paused
self._current_error: Optional[Exception] = None
self._connected: threading.Event = client._connected
self._lock: threading.Lock = threading.Lock()
if after is not None and not callable(after):
@ -664,36 +713,46 @@ class AudioPlayer(threading.Thread):
self._start = time.perf_counter()
# getattr lookup speed ups
play_audio = self.client.send_audio_packet
client = self.client
play_audio = client.send_audio_packet
self._speak(SpeakingState.voice)
while not self._end.is_set():
# are we paused?
if not self._resumed.is_set():
self.send_silence()
# wait until we aren't
self._resumed.wait()
continue
# are we disconnected from voice?
if not self._connected.is_set():
# wait until we are connected
self._connected.wait()
# reset our internal data
self.loops = 0
self._start = time.perf_counter()
self.loops += 1
data = self.source.read()
if not data:
self.stop()
break
# are we disconnected from voice?
if not client.is_connected():
_log.debug('Not connected, waiting for %ss...', client.timeout)
# wait until we are connected, but not forever
connected = client.wait_until_connected(client.timeout)
if self._end.is_set() or not connected:
_log.debug('Aborting playback')
return
_log.debug('Reconnected, resuming playback')
self._speak(SpeakingState.voice)
# reset our internal data
self.loops = 0
self._start = time.perf_counter()
play_audio(data, encode=not self.source.is_opus())
self.loops += 1
next_time = self._start + self.DELAY * self.loops
delay = max(0, self.DELAY + (next_time - time.perf_counter()))
time.sleep(delay)
self.send_silence()
def run(self) -> None:
try:
self._do_run()
@ -739,7 +798,7 @@ class AudioPlayer(threading.Thread):
def is_paused(self) -> bool:
return not self._end.is_set() and not self._resumed.is_set()
def _set_source(self, source: AudioSource) -> None:
def set_source(self, source: AudioSource) -> None:
with self._lock:
self.pause(update_speaking=False)
self.source = source
@ -750,3 +809,11 @@ class AudioPlayer(threading.Thread):
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.client.loop)
except Exception:
_log.exception("Speaking call in player failed")
def send_silence(self, count: int = 5) -> None:
try:
for n in range(count):
self.client.send_audio_packet(OPUS_SILENCE, encode=False)
except Exception:
# Any possible error (probably a socket error) is so inconsequential it's not even worth logging
pass

44
discord/raw_models.py

@ -25,11 +25,12 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Optional, Set, List, Tuple, Union
from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union
from .enums import ChannelType, try_enum
from .utils import _get_as_snowflake
from .app_commands import AppCommandPermissions
from .colour import Colour
if TYPE_CHECKING:
from .types.gateway import (
@ -57,6 +58,7 @@ if TYPE_CHECKING:
from .guild import Guild
ReactionActionEvent = Union[MessageReactionAddEvent, MessageReactionRemoveEvent]
ReactionActionType = Literal['REACTION_ADD', 'REACTION_REMOVE']
__all__ = (
@ -196,30 +198,64 @@ class RawReactionActionEvent(_RawReprMixin):
The member who added the reaction. Only available if ``event_type`` is ``REACTION_ADD`` and the reaction is inside a guild.
.. versionadded:: 1.3
message_author_id: Optional[:class:`int`]
The author ID of the message being reacted to. Only available if ``event_type`` is ``REACTION_ADD``.
.. versionadded:: 2.4
event_type: :class:`str`
The event type that triggered this action. Can be
``REACTION_ADD`` for reaction addition or
``REACTION_REMOVE`` for reaction removal.
.. versionadded:: 1.3
burst: :class:`bool`
Whether the reaction was a burst reaction, also known as a "super reaction".
.. versionadded:: 2.4
burst_colours: List[:class:`Colour`]
A list of colours used for burst reaction animation. Only available if ``burst`` is ``True``
and if ``event_type`` is ``REACTION_ADD``.
.. versionadded:: 2.0
"""
__slots__ = ('message_id', 'user_id', 'channel_id', 'guild_id', 'emoji', 'event_type', 'member')
__slots__ = (
'message_id',
'user_id',
'channel_id',
'guild_id',
'emoji',
'event_type',
'member',
'message_author_id',
'burst',
'burst_colours',
)
def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: str) -> None:
def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: ReactionActionType) -> None:
self.message_id: int = int(data['message_id'])
self.channel_id: int = int(data['channel_id'])
self.user_id: int = int(data['user_id'])
self.emoji: PartialEmoji = emoji
self.event_type: str = event_type
self.event_type: ReactionActionType = event_type
self.member: Optional[Member] = None
self.message_author_id: Optional[int] = _get_as_snowflake(data, 'message_author_id')
self.burst: bool = data.get('burst', False)
self.burst_colours: List[Colour] = [Colour.from_str(c) for c in data.get('burst_colours', [])]
try:
self.guild_id: Optional[int] = int(data['guild_id'])
except KeyError:
self.guild_id: Optional[int] = None
@property
def burst_colors(self) -> List[Colour]:
"""An alias of :attr:`burst_colours`.
.. versionadded:: 2.4
"""
return self.burst_colours
class RawReactionClearEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_reaction_clear` event.

24
discord/reaction.py

@ -74,20 +74,40 @@ class Reaction:
emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]
The reaction emoji. May be a custom emoji, or a unicode emoji.
count: :class:`int`
Number of times this reaction was made
Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`.
me: :class:`bool`
If the user sent this reaction.
message: :class:`Message`
Message this reaction is for.
me_burst: :class:`bool`
If the user sent this super reaction.
.. versionadded:: 2.4
normal_count: :class:`int`
The number of times this reaction was made using normal reactions.
This is not available in the gateway events such as :func:`on_reaction_add`
or :func:`on_reaction_remove`.
.. versionadded:: 2.4
burst_count: :class:`int`
The number of times this reaction was made using super reactions.
This is not available in the gateway events such as :func:`on_reaction_add`
or :func:`on_reaction_remove`.
.. versionadded:: 2.4
"""
__slots__ = ('message', 'count', 'emoji', 'me')
__slots__ = ('message', 'count', 'emoji', 'me', 'me_burst', 'normal_count', 'burst_count')
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_emoji_from_partial_payload(data['emoji'])
self.count: int = data.get('count', 1)
self.me: bool = data['me']
details = data.get('count_details', {})
self.normal_count: int = details.get('normal', 0)
self.burst_count: int = details.get('burst', 0)
self.me_burst: bool = data.get('me_burst', False)
def is_custom_emoji(self) -> bool:
""":class:`bool`: If this is a custom emoji."""

11
discord/role.py

@ -30,6 +30,7 @@ from .permissions import Permissions
from .colour import Colour
from .mixins import Hashable
from .utils import snowflake_time, _bytes_to_base64_data, _get_as_snowflake, MISSING
from .flags import RoleFlags
__all__ = (
'RoleTags',
@ -219,6 +220,7 @@ class Role(Hashable):
'hoist',
'guild',
'tags',
'_flags',
'_state',
)
@ -281,6 +283,7 @@ class Role(Hashable):
self.managed: bool = data.get('managed', False)
self.mentionable: bool = data.get('mentionable', False)
self.tags: Optional[RoleTags]
self._flags: int = data.get('flags', 0)
try:
self.tags = RoleTags(data['tags'])
@ -379,6 +382,14 @@ class Role(Hashable):
role_id = self.id
return [member for member in all_members if member._roles.has(role_id)]
@property
def flags(self) -> RoleFlags:
""":class:`RoleFlags`: Returns the role's flags.
.. versionadded:: 2.4
"""
return RoleFlags._from_value(self._flags)
async def _move(self, position: int, reason: Optional[str]) -> None:
if position <= 0:
raise ValueError("Cannot move role to position 0 or below")

4
discord/shard.py

@ -192,6 +192,10 @@ class Shard:
self.ws = await asyncio.wait_for(coro, timeout=60.0)
except self._handled_exceptions as e:
await self._handle_disconnect(e)
except ReconnectWebSocket as e:
_log.debug('Somehow got a signal to %s while trying to %s shard ID %s.', e.op, exc.op, self.id)
op = EventType.resume if e.resume else EventType.identify
self._queue_put(EventItem(op, self, e))
except asyncio.CancelledError:
return
except Exception as e:

200
discord/sku.py

@ -0,0 +1,200 @@
"""
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 Optional, TYPE_CHECKING
from . import utils
from .app_commands import MissingApplicationID
from .enums import try_enum, SKUType, EntitlementType
from .flags import SKUFlags
if TYPE_CHECKING:
from datetime import datetime
from .guild import Guild
from .state import ConnectionState
from .types.sku import (
SKU as SKUPayload,
Entitlement as EntitlementPayload,
)
from .user import User
__all__ = (
'SKU',
'Entitlement',
)
class SKU:
"""Represents a premium offering as a stock-keeping unit (SKU).
.. versionadded:: 2.4
Attributes
-----------
id: :class:`int`
The SKU's ID.
type: :class:`SKUType`
The type of the SKU.
application_id: :class:`int`
The ID of the application that the SKU belongs to.
name: :class:`str`
The consumer-facing name of the premium offering.
slug: :class:`str`
A system-generated URL slug based on the SKU name.
"""
__slots__ = (
'_state',
'id',
'type',
'application_id',
'name',
'slug',
'_flags',
)
def __init__(self, *, state: ConnectionState, data: SKUPayload):
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.type: SKUType = try_enum(SKUType, data['type'])
self.application_id: int = int(data['application_id'])
self.name: str = data['name']
self.slug: str = data['slug']
self._flags: int = data['flags']
def __repr__(self) -> str:
return f'<SKU id={self.id} name={self.name!r} slug={self.slug!r}>'
@property
def flags(self) -> SKUFlags:
""":class:`SKUFlags`: Returns the flags of the SKU."""
return SKUFlags._from_value(self._flags)
@property
def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the sku's creation time in UTC."""
return utils.snowflake_time(self.id)
class Entitlement:
"""Represents an entitlement from user or guild which has been granted access to a premium offering.
.. versionadded:: 2.4
Attributes
-----------
id: :class:`int`
The entitlement's ID.
sku_id: :class:`int`
The ID of the SKU that the entitlement belongs to.
application_id: :class:`int`
The ID of the application that the entitlement belongs to.
user_id: Optional[:class:`int`]
The ID of the user that is granted access to the entitlement.
type: :class:`EntitlementType`
The type of the entitlement.
deleted: :class:`bool`
Whether the entitlement has been deleted.
starts_at: Optional[:class:`datetime.datetime`]
A UTC start date which the entitlement is valid. Not present when using test entitlements.
ends_at: Optional[:class:`datetime.datetime`]
A UTC date which entitlement is no longer valid. Not present when using test entitlements.
guild_id: Optional[:class:`int`]
The ID of the guild that is granted access to the entitlement
"""
__slots__ = (
'_state',
'id',
'sku_id',
'application_id',
'user_id',
'type',
'deleted',
'starts_at',
'ends_at',
'guild_id',
)
def __init__(self, state: ConnectionState, data: EntitlementPayload):
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.sku_id: int = int(data['sku_id'])
self.application_id: int = int(data['application_id'])
self.user_id: Optional[int] = utils._get_as_snowflake(data, 'user_id')
self.type: EntitlementType = try_enum(EntitlementType, data['type'])
self.deleted: bool = data['deleted']
self.starts_at: Optional[datetime] = utils.parse_time(data.get('starts_at', None))
self.ends_at: Optional[datetime] = utils.parse_time(data.get('ends_at', None))
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
def __repr__(self) -> str:
return f'<Entitlement id={self.id} type={self.type!r} user_id={self.user_id}>'
@property
def user(self) -> Optional[User]:
"""Optional[:class:`User`]: The user that is granted access to the entitlement."""
if self.user_id is None:
return None
return self._state.get_user(self.user_id)
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild that is granted access to the entitlement."""
return self._state._get_guild(self.guild_id)
@property
def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the entitlement's creation time in UTC."""
return utils.snowflake_time(self.id)
def is_expired(self) -> bool:
""":class:`bool`: Returns ``True`` if the entitlement is expired. Will be always False for test entitlements."""
if self.ends_at is None:
return False
return utils.utcnow() >= self.ends_at
async def delete(self) -> None:
"""|coro|
Deletes the entitlement.
Raises
-------
MissingApplicationID
The application ID could not be found.
NotFound
The entitlement could not be found.
HTTPException
Deleting the entitlement failed.
"""
if self.application_id is None:
raise MissingApplicationID
await self._state.http.delete_entitlement(self.application_id, self.id)

29
discord/state.py

@ -32,6 +32,7 @@ from typing import (
Dict,
Optional,
TYPE_CHECKING,
Type,
Union,
Callable,
Any,
@ -52,6 +53,7 @@ import os
from .guild import Guild
from .activity import BaseActivity
from .sku import Entitlement
from .user import User, ClientUser
from .emoji import Emoji
from .mentions import AllowedMentions
@ -84,6 +86,8 @@ if TYPE_CHECKING:
from .http import HTTPClient
from .voice_client import VoiceProtocol
from .gateway import DiscordWebSocket
from .ui.item import Item
from .ui.dynamic import DynamicItem
from .app_commands import CommandTree, Translator
from .types.automod import AutoModerationRule, AutoModerationActionExecution
@ -259,6 +263,13 @@ class ConnectionState(Generic[ClientT]):
self.clear()
# For some reason Discord still sends emoji/sticker data in payloads
# This makes it hard to actually swap out the appropriate store methods
# So this is checked instead, it's a small penalty to pay
@property
def cache_guild_expressions(self) -> bool:
return self._intents.emojis_and_stickers
async def close(self) -> None:
for voice in self.voice_clients:
try:
@ -388,6 +399,12 @@ class ConnectionState(Generic[ClientT]):
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
return self._view_store.remove_message_tracking(message_id)
def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
self._view_store.add_dynamic_items(*items)
def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
self._view_store.remove_dynamic_items(*items)
@property
def persistent_views(self) -> Sequence[View]:
return self._view_store.persistent_views
@ -1568,6 +1585,18 @@ class ConnectionState(Generic[ClientT]):
self.dispatch('raw_typing', raw)
def parse_entitlement_create(self, data: gw.EntitlementCreateEvent) -> None:
entitlement = Entitlement(data=data, state=self)
self.dispatch('entitlement_create', entitlement)
def parse_entitlement_update(self, data: gw.EntitlementUpdateEvent) -> None:
entitlement = Entitlement(data=data, state=self)
self.dispatch('entitlement_update', entitlement)
def parse_entitlement_delete(self, data: gw.EntitlementDeleteEvent) -> None:
entitlement = Entitlement(data=data, state=self)
self.dispatch('entitlement_delete', entitlement)
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
if isinstance(channel, (TextChannel, Thread, VoiceChannel)):
return channel.guild.get_member(user_id)

11
discord/team.py

@ -27,7 +27,7 @@ from __future__ import annotations
from . import utils
from .user import BaseUser
from .asset import Asset
from .enums import TeamMembershipState, try_enum
from .enums import TeamMemberRole, TeamMembershipState, try_enum
from typing import TYPE_CHECKING, Optional, List
@ -130,14 +130,19 @@ class TeamMember(BaseUser):
The team that the member is from.
membership_state: :class:`TeamMembershipState`
The membership state of the member (e.g. invited or accepted)
role: :class:`TeamMemberRole`
The role of the member within the team.
.. versionadded:: 2.4
"""
__slots__ = ('team', 'membership_state', 'permissions')
__slots__ = ('team', 'membership_state', 'permissions', 'role')
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload) -> None:
self.team: Team = team
self.membership_state: TeamMembershipState = try_enum(TeamMembershipState, data['membership_state'])
self.permissions: List[str] = data['permissions']
self.permissions: List[str] = data.get('permissions', [])
self.role: TeamMemberRole = try_enum(TeamMemberRole, data['role'])
super().__init__(state=state, data=data['user'])
def __repr__(self) -> str:

21
discord/template.py

@ -69,6 +69,10 @@ class _PartialTemplateState:
def member_cache_flags(self):
return self.__state.member_cache_flags
@property
def cache_guild_expressions(self):
return False
def store_emoji(self, guild, packet) -> None:
return None
@ -146,18 +150,11 @@ class Template:
self.created_at: Optional[datetime.datetime] = parse_time(data.get('created_at'))
self.updated_at: Optional[datetime.datetime] = parse_time(data.get('updated_at'))
guild_id = int(data['source_guild_id'])
guild: Optional[Guild] = self._state._get_guild(guild_id)
self.source_guild: Guild
if guild is None:
source_serialised = data['serialized_source_guild']
source_serialised['id'] = guild_id
state = _PartialTemplateState(state=self._state)
# Guild expects a ConnectionState, we're passing a _PartialTemplateState
self.source_guild = Guild(data=source_serialised, state=state) # type: ignore
else:
self.source_guild = guild
source_serialised = data['serialized_source_guild']
source_serialised['id'] = int(data['source_guild_id'])
state = _PartialTemplateState(state=self._state)
# Guild expects a ConnectionState, we're passing a _PartialTemplateState
self.source_guild = Guild(data=source_serialised, state=state) # type: ignore
self.is_dirty: Optional[bool] = data.get('is_dirty', None)

11
discord/types/audit_log.py

@ -37,6 +37,7 @@ from .role import Role
from .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMode, PermissionOverwrite, ForumTag
from .threads import Thread
from .command import ApplicationCommand, ApplicationCommandPermissions
from .automod import AutoModerationTriggerMetadata
from .onboarding import PromptOption, Prompt
AuditLogEvent = Literal[
@ -94,6 +95,8 @@ AuditLogEvent = Literal[
143,
144,
145,
150,
151,
163,
164,
167,
@ -285,6 +288,12 @@ class _AuditLogChange_DefaultReactionEmoji(TypedDict):
old_value: Optional[DefaultReaction]
class _AuditLogChange_TriggerMetadata(TypedDict):
key: Literal['trigger_metadata']
new_value: Optional[AutoModerationTriggerMetadata]
old_value: Optional[AutoModerationTriggerMetadata]
class _AuditLogChange_Prompts(TypedDict):
key: Literal['prompts']
new_value: List[Prompt]
@ -319,6 +328,7 @@ AuditLogChange = Union[
_AuditLogChange_SnowflakeList,
_AuditLogChange_AvailableTags,
_AuditLogChange_DefaultReactionEmoji,
_AuditLogChange_TriggerMetadata,
_AuditLogChange_Prompts,
_AuditLogChange_Options,
]
@ -337,6 +347,7 @@ class AuditEntryInfo(TypedDict):
guild_id: Snowflake
auto_moderation_rule_name: str
auto_moderation_rule_trigger_type: str
integration_type: str
class AuditLogEntry(TypedDict):

1
discord/types/automod.py

@ -79,6 +79,7 @@ class _AutoModerationTriggerMetadataKeywordPreset(TypedDict):
class _AutoModerationTriggerMetadataMentionLimit(TypedDict):
mention_total_limit: int
mention_raid_protection_enabled: bool
AutoModerationTriggerMetadata = Union[

17
discord/types/channel.py

@ -40,7 +40,7 @@ class PermissionOverwrite(TypedDict):
deny: str
ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13, 15]
ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13, 15, 16]
ChannelType = Union[ChannelTypeWithoutThread, ThreadType]
@ -138,8 +138,7 @@ ForumOrderType = Literal[0, 1]
ForumLayoutType = Literal[0, 1, 2]
class ForumChannel(_BaseTextChannel):
type: Literal[15]
class _BaseForumChannel(_BaseTextChannel):
available_tags: List[ForumTag]
default_reaction_emoji: Optional[DefaultReaction]
default_sort_order: Optional[ForumOrderType]
@ -147,7 +146,17 @@ class ForumChannel(_BaseTextChannel):
flags: NotRequired[int]
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel]
class ForumChannel(_BaseForumChannel):
type: Literal[15]
class MediaChannel(_BaseForumChannel):
type: Literal[16]
GuildChannel = Union[
TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel, MediaChannel
]
class _BaseDMChannel(_BaseChannel):

11
discord/types/components.py

@ -33,6 +33,7 @@ from .channel import ChannelType
ComponentType = Literal[1, 2, 3, 4]
ButtonStyle = Literal[1, 2, 3, 4, 5]
TextStyle = Literal[1, 2]
DefaultValueType = Literal['user', 'role', 'channel']
class ActionRow(TypedDict):
@ -66,6 +67,11 @@ class SelectComponent(TypedDict):
disabled: NotRequired[bool]
class SelectDefaultValues(TypedDict):
id: int
type: DefaultValueType
class StringSelectComponent(SelectComponent):
type: Literal[3]
options: NotRequired[List[SelectOption]]
@ -73,19 +79,23 @@ class StringSelectComponent(SelectComponent):
class UserSelectComponent(SelectComponent):
type: Literal[5]
default_values: NotRequired[List[SelectDefaultValues]]
class RoleSelectComponent(SelectComponent):
type: Literal[6]
default_values: NotRequired[List[SelectDefaultValues]]
class MentionableSelectComponent(SelectComponent):
type: Literal[7]
default_values: NotRequired[List[SelectDefaultValues]]
class ChannelSelectComponent(SelectComponent):
type: Literal[8]
channel_types: NotRequired[List[ChannelType]]
default_values: NotRequired[List[SelectDefaultValues]]
class TextInput(TypedDict):
@ -104,6 +114,7 @@ class SelectMenu(SelectComponent):
type: Literal[3, 5, 6, 7, 8]
options: NotRequired[List[SelectOption]]
channel_types: NotRequired[List[ChannelType]]
default_values: NotRequired[List[SelectDefaultValues]]
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]

8
discord/types/gateway.py

@ -27,6 +27,7 @@ from typing_extensions import NotRequired, Required
from .automod import AutoModerationAction, AutoModerationRuleTriggerType
from .activity import PartialPresenceUpdate
from .sku import Entitlement
from .voice import GuildVoiceState
from .integration import BaseIntegration, IntegrationApplication
from .role import Role
@ -100,6 +101,9 @@ class MessageReactionAddEvent(TypedDict):
emoji: PartialEmoji
member: NotRequired[MemberWithUser]
guild_id: NotRequired[Snowflake]
message_author_id: NotRequired[Snowflake]
burst: bool
burst_colors: NotRequired[List[str]]
class MessageReactionRemoveEvent(TypedDict):
@ -108,6 +112,7 @@ class MessageReactionRemoveEvent(TypedDict):
message_id: Snowflake
emoji: PartialEmoji
guild_id: NotRequired[Snowflake]
burst: bool
class MessageReactionRemoveAllEvent(TypedDict):
@ -343,3 +348,6 @@ class AutoModerationActionExecution(TypedDict):
class GuildAuditLogEntryCreate(AuditLogEntry):
guild_id: Snowflake
EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement

3
discord/types/interactions.py

@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union
from typing_extensions import NotRequired
from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel
from .sku import Entitlement
from .threads import ThreadType
from .member import Member
from .message import Attachment
@ -208,6 +209,8 @@ class _BaseInteraction(TypedDict):
app_permissions: NotRequired[str]
locale: NotRequired[str]
guild_locale: NotRequired[str]
entitlement_sku_ids: NotRequired[List[Snowflake]]
entitlements: NotRequired[List[Entitlement]]
class PingInteraction(_BaseInteraction):

9
discord/types/message.py

@ -50,10 +50,18 @@ class ChannelMention(TypedDict):
name: str
class ReactionCountDetails(TypedDict):
burst: int
normal: int
class Reaction(TypedDict):
count: int
me: bool
emoji: PartialEmoji
me_burst: bool
count_details: ReactionCountDetails
burst_colors: List[str]
class Attachment(TypedDict):
@ -70,6 +78,7 @@ class Attachment(TypedDict):
ephemeral: NotRequired[bool]
duration_secs: NotRequired[float]
waveform: NotRequired[str]
flags: NotRequired[int]
MessageActivityType = Literal[1, 2, 3, 5]

1
discord/types/role.py

@ -39,6 +39,7 @@ class Role(TypedDict):
permissions: str
managed: bool
mentionable: bool
flags: int
icon: NotRequired[Optional[str]]
unicode_emoji: NotRequired[Optional[str]]
tags: NotRequired[RoleTags]

52
discord/types/sku.py

@ -0,0 +1,52 @@
"""
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 TypedDict, Optional, Literal
from typing_extensions import NotRequired
class SKU(TypedDict):
id: str
type: int
application_id: str
name: str
slug: str
flags: int
class Entitlement(TypedDict):
id: str
sku_id: str
application_id: str
user_id: Optional[str]
type: int
deleted: bool
starts_at: NotRequired[str]
ends_at: NotRequired[str]
guild_id: Optional[str]
EntitlementOwnerType = Literal[1, 2]

2
discord/types/sticker.py

@ -30,7 +30,7 @@ from typing_extensions import NotRequired
from .snowflake import Snowflake
from .user import User
StickerFormatType = Literal[1, 2, 3]
StickerFormatType = Literal[1, 2, 3, 4]
class StickerItem(TypedDict):

3
discord/types/team.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TypedDict, List, Optional
from typing import Literal, TypedDict, List, Optional
from .user import PartialUser
from .snowflake import Snowflake
@ -35,6 +35,7 @@ class TeamMember(TypedDict):
membership_state: int
permissions: List[str]
team_id: Snowflake
role: Literal['admin', 'developer', 'read_only']
class Team(TypedDict):

2
discord/types/user.py

@ -34,7 +34,7 @@ class PartialUser(TypedDict):
global_name: Optional[str]
PremiumType = Literal[0, 1, 2]
PremiumType = Literal[0, 1, 2, 3]
class User(PartialUser, total=False):

1
discord/ui/__init__.py

@ -15,3 +15,4 @@ from .item import *
from .button import *
from .select import *
from .text_input import *
from .dynamic import *

209
discord/ui/dynamic.py

@ -0,0 +1,209 @@
"""
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 ClassVar, Dict, Generic, Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Any, Union
import re
from .item import Item
from .._types import ClientT
__all__ = ('DynamicItem',)
BaseT = TypeVar('BaseT', bound='Item[Any]', covariant=True)
if TYPE_CHECKING:
from typing_extensions import TypeVar, Self
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)
else:
V = TypeVar('V', bound='View', covariant=True)
class DynamicItem(Generic[BaseT], Item['View']):
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
that ``custom_id``.
The ``custom_id`` parsing is done using the ``re`` module by passing a ``template``
parameter to the class parameter list.
This item is generated every time the component is dispatched. This means that
any variable that holds an instance of this class will eventually be out of date
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.
.. versionadded:: 2.4
Parameters
------------
item: :class:`Item`
The item to wrap with dynamic custom ID parsing.
template: Union[:class:`str`, ``re.Pattern``]
The template to use for parsing the ``custom_id``. This can be a string or a compiled
regular expression. This must be passed as a keyword argument to the class creation.
row: Optional[:class:`int`]
The relative row this button 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).
Attributes
-----------
item: :class:`Item`
The item that is wrapped with dynamic custom ID parsing.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'item',
'template',
)
__discord_ui_compiled_template__: ClassVar[re.Pattern[str]]
def __init_subclass__(cls, *, template: Union[str, re.Pattern[str]]) -> None:
super().__init_subclass__()
cls.__discord_ui_compiled_template__ = re.compile(template) if isinstance(template, str) else template
if not isinstance(cls.__discord_ui_compiled_template__, re.Pattern):
raise TypeError('template must be a str or a re.Pattern')
def __init__(
self,
item: BaseT,
*,
row: Optional[int] = None,
) -> None:
super().__init__()
self.item: BaseT = item
self.row = row
if not self.item.is_dispatchable():
raise TypeError('item must be dispatchable, e.g. not a URL button')
if not self.template.match(self.custom_id):
raise ValueError(f'item custom_id {self.custom_id!r} must match the template {self.template.pattern!r}')
@property
def template(self) -> re.Pattern[str]:
"""``re.Pattern``: The compiled regular expression that is used to parse the ``custom_id``."""
return self.__class__.__discord_ui_compiled_template__
def to_component_dict(self) -> Dict[str, Any]:
return self.item.to_component_dict()
def _refresh_component(self, component: Component) -> None:
self.item._refresh_component(component)
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
self.item._refresh_state(interaction, data)
@classmethod
def from_component(cls: Type[Self], component: Component) -> Self:
raise TypeError('Dynamic items cannot be created from components')
@property
def type(self) -> ComponentType:
return self.item.type
def is_dispatchable(self) -> bool:
return self.item.is_dispatchable()
def is_persistent(self) -> bool:
return True
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the dynamic item that gets received during an interaction."""
return self.item.custom_id # type: ignore # This attribute exists for dispatchable items
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
if not self.template.match(value):
raise ValueError(f'custom_id must match the template {self.template.pattern!r}')
self.item.custom_id = value # type: ignore # This attribute exists for dispatchable items
self._provided_custom_id = True
@property
def row(self) -> Optional[int]:
return self.item._row
@row.setter
def row(self, value: Optional[int]) -> None:
self.item.row = value
@property
def width(self) -> int:
return self.item.width
@classmethod
async def from_custom_id(
cls: Type[Self], interaction: Interaction[ClientT], item: Item[Any], match: re.Match[str], /
) -> Self:
"""|coro|
A classmethod that is called when the ``custom_id`` of a component matches the
``template`` of the class. This is called when the component is dispatched.
It must return a new instance of the :class:`DynamicItem`.
Subclasses *must* implement this method.
Exceptions raised in this method are logged and ignored.
.. warning::
This method is called before the callback is dispatched, therefore
it means that it is subject to the same timing restrictions as the callback.
Ergo, you must reply to an interaction within 3 seconds of it being
dispatched.
Parameters
------------
interaction: :class:`~discord.Interaction`
The interaction that the component belongs to.
item: :class:`~discord.ui.Item`
The base item that is being dispatched.
match: ``re.Match``
The match object that was created from the ``template``
matching the ``custom_id``.
Returns
--------
:class:`DynamicItem`
The new instance of the :class:`DynamicItem` with information
from the ``match`` object.
"""
raise NotImplementedError

33
discord/ui/item.py

@ -133,3 +133,36 @@ class Item(Generic[V]):
The interaction that triggered this UI item.
"""
pass
async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool:
"""|coro|
A callback that is called when an interaction happens within this item
that checks whether the callback should be processed.
This is useful to override if, for example, you want to ensure that the
interaction author is a given user.
The default implementation of this returns ``True``.
.. note::
If an exception occurs within the body then the check
is considered a failure and :meth:`discord.ui.View.on_error` is called.
For :class:`~discord.ui.DynamicItem` this does not call the ``on_error``
handler.
.. versionadded:: 2.4
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction that occurred.
Returns
---------
:class:`bool`
Whether the callback should be called.
"""
return True

256
discord/ui/select.py

@ -22,21 +22,42 @@ 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 Any, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Callable, Union, Dict, overload
from typing import (
Any,
List,
Literal,
Optional,
TYPE_CHECKING,
Tuple,
Type,
TypeVar,
Callable,
Union,
Dict,
overload,
Sequence,
)
from contextvars import ContextVar
import inspect
import os
from .item import Item, ItemCallbackType
from ..enums import ChannelType, ComponentType
from ..enums import ChannelType, ComponentType, SelectDefaultValueType
from ..partial_emoji import PartialEmoji
from ..emoji import Emoji
from ..utils import MISSING
from ..utils import MISSING, _human_join
from ..components import (
SelectOption,
SelectMenu,
SelectDefaultValue,
)
from ..app_commands.namespace import Namespace
from ..member import Member
from ..object import Object
from ..role import Role
from ..user import User, ClientUser
from ..abc import GuildChannel
from ..threads import Thread
__all__ = (
'Select',
@ -48,15 +69,12 @@ __all__ = (
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias, Self
from typing_extensions import TypeAlias, Self, TypeGuard
from .view import View
from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import SelectMessageComponentInteractionData
from ..app_commands import AppCommandChannel, AppCommandThread
from ..member import Member
from ..role import Role
from ..user import User
from ..interactions import Interaction
ValidSelectType: TypeAlias = Literal[
@ -69,6 +87,18 @@ if TYPE_CHECKING:
PossibleValue: TypeAlias = Union[
str, User, Member, Role, AppCommandChannel, AppCommandThread, Union[Role, Member], Union[Role, User]
]
ValidDefaultValues: TypeAlias = Union[
SelectDefaultValue,
Object,
Role,
Member,
ClientUser,
User,
GuildChannel,
AppCommandChannel,
AppCommandThread,
Thread,
]
V = TypeVar('V', bound='View', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
@ -78,10 +108,81 @@ 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]
DefaultSelectComponentTypes = Literal[
ComponentType.user_select,
ComponentType.role_select,
ComponentType.channel_select,
ComponentType.mentionable_select,
]
selected_values: ContextVar[Dict[str, List[PossibleValue]]] = ContextVar('selected_values')
def _is_valid_object_type(
obj: Any,
component_type: DefaultSelectComponentTypes,
type_to_supported_classes: Dict[ValidSelectType, Tuple[Type[ValidDefaultValues], ...]],
) -> TypeGuard[Type[ValidDefaultValues]]:
return issubclass(obj, type_to_supported_classes[component_type])
def _handle_select_defaults(
defaults: Sequence[ValidDefaultValues], component_type: DefaultSelectComponentTypes
) -> List[SelectDefaultValue]:
if not defaults or defaults is MISSING:
return []
from ..app_commands import AppCommandChannel, AppCommandThread
cls_to_type: Dict[Type[ValidDefaultValues], SelectDefaultValueType] = {
User: SelectDefaultValueType.user,
Member: SelectDefaultValueType.user,
ClientUser: SelectDefaultValueType.user,
Role: SelectDefaultValueType.role,
GuildChannel: SelectDefaultValueType.channel,
AppCommandChannel: SelectDefaultValueType.channel,
AppCommandThread: SelectDefaultValueType.channel,
Thread: SelectDefaultValueType.channel,
}
type_to_supported_classes: Dict[ValidSelectType, Tuple[Type[ValidDefaultValues], ...]] = {
ComponentType.user_select: (User, ClientUser, Member, Object),
ComponentType.role_select: (Role, Object),
ComponentType.channel_select: (GuildChannel, AppCommandChannel, AppCommandThread, Thread, Object),
ComponentType.mentionable_select: (User, ClientUser, Member, Role, Object),
}
values: List[SelectDefaultValue] = []
for obj in defaults:
if isinstance(obj, SelectDefaultValue):
values.append(obj)
continue
object_type = obj.__class__ if not isinstance(obj, Object) else obj.type
if not _is_valid_object_type(object_type, component_type, type_to_supported_classes):
supported_classes = _human_join([c.__name__ for c in type_to_supported_classes[component_type]])
raise TypeError(f'Expected an instance of {supported_classes} not {object_type.__name__}')
if object_type is Object:
if component_type is ComponentType.mentionable_select:
raise ValueError(
'Object must have a type specified for the chosen select type. Please pass one using the `type`` kwarg.'
)
elif component_type is ComponentType.user_select:
object_type = User
elif component_type is ComponentType.role_select:
object_type = Role
elif component_type is ComponentType.channel_select:
object_type = GuildChannel
if issubclass(object_type, GuildChannel):
object_type = GuildChannel
values.append(SelectDefaultValue(id=obj.id, type=cls_to_type[object_type]))
return values
class BaseSelect(Item[V]):
"""The base Select model that all other Select models inherit from.
@ -115,6 +216,13 @@ class BaseSelect(Item[V]):
'max_values',
'disabled',
)
__component_attributes__: Tuple[str, ...] = (
'custom_id',
'placeholder',
'min_values',
'max_values',
'disabled',
)
def __init__(
self,
@ -128,6 +236,7 @@ class BaseSelect(Item[V]):
disabled: bool = False,
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING,
default_values: Sequence[SelectDefaultValue] = MISSING,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not MISSING
@ -144,6 +253,7 @@ class BaseSelect(Item[V]):
disabled=disabled,
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,
)
self.row = row
@ -233,10 +343,16 @@ class BaseSelect(Item[V]):
@classmethod
def from_component(cls, component: SelectMenu) -> Self:
return cls(
**{k: getattr(component, k) for k in cls.__item_repr_attributes__},
row=None,
)
type_to_cls: Dict[ComponentType, Type[BaseSelect[Any]]] = {
ComponentType.string_select: Select,
ComponentType.user_select: UserSelect,
ComponentType.role_select: RoleSelect,
ComponentType.channel_select: ChannelSelect,
ComponentType.mentionable_select: MentionableSelect,
}
constructor = type_to_cls.get(component.type, Select)
kwrgs = {key: getattr(component, key) for key in constructor.__component_attributes__}
return constructor(**kwrgs)
class Select(BaseSelect[V]):
@ -270,7 +386,7 @@ class Select(BaseSelect[V]):
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__item_repr_attributes__ = BaseSelect.__item_repr_attributes__ + ('options',)
__component_attributes__ = BaseSelect.__component_attributes__ + ('options',)
def __init__(
self,
@ -409,6 +525,10 @@ class UserSelect(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the users that should be selected by default.
.. versionadded:: 2.4
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
@ -417,6 +537,8 @@ class UserSelect(BaseSelect[V]):
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__(
self,
*,
@ -426,6 +548,7 @@ class UserSelect(BaseSelect[V]):
max_values: int = 1,
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None:
super().__init__(
self.type,
@ -435,6 +558,7 @@ class UserSelect(BaseSelect[V]):
max_values=max_values,
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
)
@property
@ -455,6 +579,18 @@ class UserSelect(BaseSelect[V]):
"""
return super().values # type: ignore
@property
def default_values(self) -> List[SelectDefaultValue]:
"""List[:class:`discord.SelectDefaultValue`]: A list of default values for the select menu.
.. versionadded:: 2.4
"""
return self._underlying.default_values
@default_values.setter
def default_values(self, value: Sequence[ValidDefaultValues]) -> None:
self._underlying.default_values = _handle_select_defaults(value, self.type)
class RoleSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current roles of the guild.
@ -478,6 +614,10 @@ class RoleSelect(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the users that should be selected by default.
.. versionadded:: 2.4
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
@ -486,6 +626,8 @@ class RoleSelect(BaseSelect[V]):
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__(
self,
*,
@ -495,6 +637,7 @@ class RoleSelect(BaseSelect[V]):
max_values: int = 1,
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None:
super().__init__(
self.type,
@ -504,6 +647,7 @@ class RoleSelect(BaseSelect[V]):
max_values=max_values,
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
)
@property
@ -516,6 +660,18 @@ class RoleSelect(BaseSelect[V]):
"""List[:class:`discord.Role`]: A list of roles that have been selected by the user."""
return super().values # type: ignore
@property
def default_values(self) -> List[SelectDefaultValue]:
"""List[:class:`discord.SelectDefaultValue`]: A list of default values for the select menu.
.. versionadded:: 2.4
"""
return self._underlying.default_values
@default_values.setter
def default_values(self, value: Sequence[ValidDefaultValues]) -> None:
self._underlying.default_values = _handle_select_defaults(value, self.type)
class MentionableSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current members and roles in the guild.
@ -542,6 +698,11 @@ class MentionableSelect(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the users/roles that should be selected by default.
if :class:`.Object` is passed, then the type must be specified in the constructor.
.. versionadded:: 2.4
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
@ -550,6 +711,8 @@ class MentionableSelect(BaseSelect[V]):
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__(
self,
*,
@ -559,6 +722,7 @@ class MentionableSelect(BaseSelect[V]):
max_values: int = 1,
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None:
super().__init__(
self.type,
@ -568,6 +732,7 @@ class MentionableSelect(BaseSelect[V]):
max_values=max_values,
disabled=disabled,
row=row,
default_values=_handle_select_defaults(default_values, self.type),
)
@property
@ -588,6 +753,18 @@ class MentionableSelect(BaseSelect[V]):
"""
return super().values # type: ignore
@property
def default_values(self) -> List[SelectDefaultValue]:
"""List[:class:`discord.SelectDefaultValue`]: A list of default values for the select menu.
.. versionadded:: 2.4
"""
return self._underlying.default_values
@default_values.setter
def default_values(self, value: Sequence[ValidDefaultValues]) -> None:
self._underlying.default_values = _handle_select_defaults(value, self.type)
class ChannelSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current channels in the guild.
@ -613,6 +790,10 @@ class ChannelSelect(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select is disabled or not.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the channels that should be selected by default.
.. versionadded:: 2.4
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
@ -621,7 +802,10 @@ class ChannelSelect(BaseSelect[V]):
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__item_repr_attributes__ = BaseSelect.__item_repr_attributes__ + ('channel_types',)
__component_attributes__ = BaseSelect.__component_attributes__ + (
'channel_types',
'default_values',
)
def __init__(
self,
@ -633,6 +817,7 @@ class ChannelSelect(BaseSelect[V]):
max_values: int = 1,
disabled: bool = False,
row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None:
super().__init__(
self.type,
@ -643,6 +828,7 @@ class ChannelSelect(BaseSelect[V]):
disabled=disabled,
row=row,
channel_types=channel_types,
default_values=_handle_select_defaults(default_values, self.type),
)
@property
@ -669,6 +855,18 @@ class ChannelSelect(BaseSelect[V]):
"""List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]]: A list of channels selected by the user."""
return super().values # type: ignore
@property
def default_values(self) -> List[SelectDefaultValue]:
"""List[:class:`discord.SelectDefaultValue`]: A list of default values for the select menu.
.. versionadded:: 2.4
"""
return self._underlying.default_values
@default_values.setter
def default_values(self, value: Sequence[ValidDefaultValues]) -> None:
self._underlying.default_values = _handle_select_defaults(value, self.type)
@overload
def select(
@ -697,6 +895,7 @@ def select(
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, UserSelectT]:
...
@ -713,6 +912,7 @@ def select(
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, RoleSelectT]:
...
@ -729,6 +929,7 @@ def select(
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, ChannelSelectT]:
...
@ -745,6 +946,7 @@ def select(
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, MentionableSelectT]:
...
@ -760,6 +962,7 @@ def select(
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Sequence[ValidDefaultValues] = MISSING,
row: Optional[int] = None,
) -> SelectCallbackDecorator[V, BaseSelectT]:
"""A decorator that attaches a select menu to a component.
@ -831,6 +1034,11 @@ def select(
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.
.. versionadded:: 2.4
"""
def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]:
@ -838,8 +1046,8 @@ def select(
raise TypeError('select function must be a coroutine function')
callback_cls = getattr(cls, '__origin__', cls)
if not issubclass(callback_cls, BaseSelect):
supported_classes = ", ".join(["ChannelSelect", "MentionableSelect", "RoleSelect", "Select", "UserSelect"])
raise TypeError(f'cls must be one of {supported_classes} or a subclass of one of them, not {cls!r}.')
supported_classes = ', '.join(['ChannelSelect', 'MentionableSelect', 'RoleSelect', 'Select', 'UserSelect'])
raise TypeError(f'cls must be one of {supported_classes} or a subclass of one of them, not {cls.__name__}.')
func.__discord_ui_model_type__ = callback_cls
func.__discord_ui_model_kwargs__ = {
@ -854,6 +1062,24 @@ def select(
func.__discord_ui_model_kwargs__['options'] = options
if issubclass(callback_cls, ChannelSelect):
func.__discord_ui_model_kwargs__['channel_types'] = channel_types
if not issubclass(callback_cls, Select):
cls_to_type: Dict[
Type[BaseSelect],
Literal[
ComponentType.user_select,
ComponentType.channel_select,
ComponentType.role_select,
ComponentType.mentionable_select,
],
] = {
UserSelect: ComponentType.user_select,
RoleSelect: ComponentType.role_select,
MentionableSelect: ComponentType.mentionable_select,
ChannelSelect: ComponentType.channel_select,
}
func.__discord_ui_model_kwargs__['default_values'] = (
MISSING if default_values is MISSING else _handle_select_defaults(default_values, cls_to_type[callback_cls])
)
return func

94
discord/ui/view.py

@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type
from functools import partial
from itertools import groupby
@ -33,6 +33,7 @@ import sys
import time
import os
from .item import Item, ItemCallbackType
from .dynamic import DynamicItem
from ..components import (
Component,
ActionRow as ActionRowComponent,
@ -50,6 +51,7 @@ __all__ = (
if TYPE_CHECKING:
from typing_extensions import Self
import re
from ..interactions import Interaction
from ..message import Message
@ -76,9 +78,10 @@ def _component_to_item(component: Component) -> Item:
return Button.from_component(component)
if isinstance(component, SelectComponent):
from .select import Select
from .select import BaseSelect
return BaseSelect.from_component(component)
return Select.from_component(component)
return Item.from_component(component)
@ -417,7 +420,7 @@ class View:
try:
item._refresh_state(interaction, interaction.data) # type: ignore
allow = await self.interaction_check(interaction)
allow = await item.interaction_check(interaction) and await self.interaction_check(interaction)
if not allow:
return
@ -534,6 +537,8 @@ class ViewStore:
self._synced_message_views: Dict[int, View] = {}
# 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
@property
@ -548,6 +553,16 @@ class ViewStore:
# fmt: on
return list(views.values())
def add_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
for item in items:
pattern = item.__discord_ui_compiled_template__
self._dynamic_items[pattern] = item
def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
for item in items:
pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None)
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
view._start_listening_from_store(self)
if view.__discord_ui_modal__:
@ -555,12 +570,17 @@ class ViewStore:
return
dispatch_info = self._views.setdefault(message_id, {})
is_fully_dynamic = True
for item in view._children:
if item.is_dispatchable():
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items[pattern] = item.__class__
elif item.is_dispatchable():
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
is_fully_dynamic = False
view._cache_key = message_id
if message_id is not None:
if message_id is not None and not is_fully_dynamic:
self._synced_message_views[message_id] = view
def remove_view(self, view: View) -> None:
@ -571,7 +591,10 @@ class ViewStore:
dispatch_info = self._views.get(view._cache_key)
if dispatch_info:
for item in view._children:
if item.is_dispatchable():
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None)
elif item.is_dispatchable():
dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
if len(dispatch_info) == 0:
@ -579,7 +602,64 @@ class ViewStore:
self._synced_message_views.pop(view._cache_key, None) # type: ignore
async def schedule_dynamic_item_call(
self,
component_type: int,
factory: Type[DynamicItem[Item[Any]]],
interaction: Interaction,
custom_id: str,
match: re.Match[str],
) -> None:
if interaction.message is None:
return
view = View.from_message(interaction.message)
base_item_index: Optional[int] = None
for index, child in enumerate(view._children):
if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id:
base_item_index = index
break
if base_item_index is None:
return
base_item = view._children[base_item_index]
try:
item = await factory.from_custom_id(interaction, base_item, match)
except Exception:
_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
item._view = view
item._refresh_state(interaction, interaction.data) # type: ignore
try:
allow = await item.interaction_check(interaction)
except Exception:
allow = False
if not allow:
return
try:
await item.callback(interaction)
except Exception:
_log.exception('Ignoring exception in dynamic item callback for %r', item)
def dispatch_dynamic_items(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
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}',
)
def dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
self.dispatch_dynamic_items(component_type, custom_id, interaction)
interaction_id: Optional[int] = None
message_id: Optional[int] = None
# Realistically, in a component based interaction the Interaction.message will never be None

20
discord/utils.py

@ -899,13 +899,13 @@ def resolve_template(code: Union[Template, str]) -> str:
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c) for c in ('*', '`', '_', '~', '|'))
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)|^#{1,3}|^\s*-'
_MARKDOWN_ESCAPE_REGEX = re.compile(fr'(?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 = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
def remove_markdown(text: str, *, ignore_links: bool = True) -> str:
@ -932,7 +932,7 @@ def remove_markdown(text: str, *, ignore_links: bool = True) -> str:
The text with the markdown special characters removed.
"""
def replacement(match):
def replacement(match: re.Match[str]) -> str:
groupdict = match.groupdict()
return groupdict.get('url', '')
@ -1380,3 +1380,17 @@ CAMEL_CASE_REGEX = re.compile(r'(?<!^)(?=[A-Z])')
def _to_kebab_case(text: str) -> str:
return CAMEL_CASE_REGEX.sub('-', text).lower()
def _human_join(seq: Sequence[str], /, *, delimiter: str = ', ', final: str = 'or') -> str:
size = len(seq)
if size == 0:
return ''
if size == 1:
return seq[0]
if size == 2:
return f'{seq[0]} {final} {seq[1]}'
return delimiter.join(seq[:-1]) + f' {final} {seq[-1]}'

361
discord/voice_client.py

@ -20,45 +20,29 @@ 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.
Some documentation to refer to:
- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID.
- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE.
- We pull the session_id from VOICE_STATE_UPDATE.
- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE.
- Then we initiate the voice web socket (vWS) pointing to the endpoint.
- We send opcode 0 with the user_id, server_id, session_id and token using the vWS.
- The vWS sends back opcode 2 with an ssrc, port, modes(array) and heartbeat_interval.
- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE.
- Then we send our IP and port via vWS with opcode 1.
- When that's all done, we receive opcode 4 from the vWS.
- Finally we can transmit data to endpoint:port.
"""
from __future__ import annotations
import asyncio
import socket
import logging
import struct
import threading
from typing import Any, Callable, List, Optional, TYPE_CHECKING, Tuple, Union
from . import opus, utils
from .backoff import ExponentialBackoff
from . import opus
from .gateway import *
from .errors import ClientException, ConnectionClosed
from .errors import ClientException
from .player import AudioPlayer, AudioSource
from .utils import MISSING
from .voice_state import VoiceConnectionState
if TYPE_CHECKING:
from .gateway import DiscordVoiceWebSocket
from .client import Client
from .guild import Guild
from .state import ConnectionState
from .user import ClientUser
from .opus import Encoder
from .opus import Encoder, APPLICATION_CTL, BAND_CTL, SIGNAL_CTL
from .channel import StageChannel, VoiceChannel
from . import abc
@ -226,12 +210,6 @@ class VoiceClient(VoiceProtocol):
"""
channel: VocalGuildChannel
endpoint_ip: str
voice_port: int
ip: str
port: int
secret_key: List[int]
ssrc: int
def __init__(self, client: Client, channel: abc.Connectable) -> None:
if not has_nacl:
@ -239,29 +217,18 @@ class VoiceClient(VoiceProtocol):
super().__init__(client, channel)
state = client._connection
self.token: str = MISSING
self.server_id: int = MISSING
self.socket = MISSING
self.loop: asyncio.AbstractEventLoop = state.loop
self._state: ConnectionState = state
# this will be used in the AudioPlayer thread
self._connected: threading.Event = threading.Event()
self._handshaking: bool = False
self._potentially_reconnecting: bool = False
self._voice_state_complete: asyncio.Event = asyncio.Event()
self._voice_server_complete: asyncio.Event = asyncio.Event()
self.mode: str = MISSING
self._connections: int = 0
self.sequence: int = 0
self.timestamp: int = 0
self.timeout: float = 0
self._runner: asyncio.Task = MISSING
self._player: Optional[AudioPlayer] = None
self.encoder: Encoder = MISSING
self._lite_nonce: int = 0
self.ws: DiscordVoiceWebSocket = MISSING
self._connection: VoiceConnectionState = self.create_connection_state()
warn_nacl: bool = not has_nacl
supported_modes: Tuple[SupportedModes, ...] = (
@ -280,6 +247,38 @@ class VoiceClient(VoiceProtocol):
""":class:`ClientUser`: The user connected to voice (i.e. ourselves)."""
return self._state.user # type: ignore
@property
def session_id(self) -> Optional[str]:
return self._connection.session_id
@property
def token(self) -> Optional[str]:
return self._connection.token
@property
def endpoint(self) -> Optional[str]:
return self._connection.endpoint
@property
def ssrc(self) -> int:
return self._connection.ssrc
@property
def mode(self) -> SupportedModes:
return self._connection.mode
@property
def secret_key(self) -> List[int]:
return self._connection.secret_key
@property
def ws(self) -> DiscordVoiceWebSocket:
return self._connection.ws
@property
def timeout(self) -> float:
return self._connection.timeout
def checked_add(self, attr: str, value: int, limit: int) -> None:
val = getattr(self, attr)
if val + value > limit:
@ -289,144 +288,23 @@ class VoiceClient(VoiceProtocol):
# connection related
def create_connection_state(self) -> VoiceConnectionState:
return VoiceConnectionState(self)
async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None:
self.session_id: str = data['session_id']
channel_id = data['channel_id']
if not self._handshaking or self._potentially_reconnecting:
# If we're done handshaking then we just need to update ourselves
# If we're potentially reconnecting due to a 4014, then we need to differentiate
# a channel move and an actual force disconnect
if channel_id is None:
# We're being disconnected so cleanup
await self.disconnect()
else:
self.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore
else:
self._voice_state_complete.set()
await self._connection.voice_state_update(data)
async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
if self._voice_server_complete.is_set():
_log.warning('Ignoring extraneous voice server update.')
return
self.token = data['token']
self.server_id = int(data['guild_id'])
endpoint = data.get('endpoint')
if endpoint is None or self.token is None:
_log.warning(
'Awaiting endpoint... This requires waiting. '
'If timeout occurred considering raising the timeout and reconnecting.'
)
return
self.endpoint, _, _ = endpoint.rpartition(':')
if self.endpoint.startswith('wss://'):
# Just in case, strip it off since we're going to add it later
self.endpoint: str = self.endpoint[6:]
# This gets set later
self.endpoint_ip = MISSING
self.socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False)
if not self._handshaking:
# If we're not handshaking then we need to terminate our previous connection in the websocket
await self.ws.close(4000)
return
self._voice_server_complete.set()
async def voice_connect(self, self_deaf: bool = False, self_mute: bool = False) -> None:
await self.channel.guild.change_voice_state(channel=self.channel, self_deaf=self_deaf, self_mute=self_mute)
async def voice_disconnect(self) -> None:
_log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id)
await self.channel.guild.change_voice_state(channel=None)
def prepare_handshake(self) -> None:
self._voice_state_complete.clear()
self._voice_server_complete.clear()
self._handshaking = True
_log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1)
self._connections += 1
def finish_handshake(self) -> None:
_log.info('Voice handshake complete. Endpoint found %s', self.endpoint)
self._handshaking = False
self._voice_server_complete.clear()
self._voice_state_complete.clear()
async def connect_websocket(self) -> DiscordVoiceWebSocket:
ws = await DiscordVoiceWebSocket.from_client(self)
self._connected.clear()
while ws.secret_key is None:
await ws.poll_event()
self._connected.set()
return ws
await self._connection.voice_server_update(data)
async def connect(self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False) -> None:
_log.info('Connecting to voice...')
self.timeout = timeout
for i in range(5):
self.prepare_handshake()
# This has to be created before we start the flow.
futures = [
self._voice_state_complete.wait(),
self._voice_server_complete.wait(),
]
# Start the connection flow
await self.voice_connect(self_deaf=self_deaf, self_mute=self_mute)
try:
await utils.sane_wait_for(futures, timeout=timeout)
except asyncio.TimeoutError:
await self.disconnect(force=True)
raise
self.finish_handshake()
try:
self.ws = await self.connect_websocket()
break
except (ConnectionClosed, asyncio.TimeoutError):
if reconnect:
_log.exception('Failed to connect to voice... Retrying...')
await asyncio.sleep(1 + i * 2.0)
await self.voice_disconnect()
continue
else:
raise
if self._runner is MISSING:
self._runner = self.client.loop.create_task(self.poll_voice_ws(reconnect))
async def potential_reconnect(self) -> bool:
# Attempt to stop the player thread from playing early
self._connected.clear()
self.prepare_handshake()
self._potentially_reconnecting = True
try:
# We only care about VOICE_SERVER_UPDATE since VOICE_STATE_UPDATE can come before we get disconnected
await asyncio.wait_for(self._voice_server_complete.wait(), timeout=self.timeout)
except asyncio.TimeoutError:
self._potentially_reconnecting = False
await self.disconnect(force=True)
return False
self.finish_handshake()
self._potentially_reconnecting = False
try:
self.ws = await self.connect_websocket()
except (ConnectionClosed, asyncio.TimeoutError):
return False
else:
return True
await self._connection.connect(
reconnect=reconnect, timeout=timeout, self_deaf=self_deaf, self_mute=self_mute, resume=False
)
def wait_until_connected(self, timeout: Optional[float] = 30.0) -> bool:
self._connection.wait(timeout)
return self._connection.is_connected()
@property
def latency(self) -> float:
@ -437,7 +315,7 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4
"""
ws = self.ws
ws = self._connection.ws
return float("inf") if not ws else ws.latency
@property
@ -446,72 +324,19 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4
"""
ws = self.ws
ws = self._connection.ws
return float("inf") if not ws else ws.average_latency
async def poll_voice_ws(self, reconnect: bool) -> None:
backoff = ExponentialBackoff()
while True:
try:
await self.ws.poll_event()
except (ConnectionClosed, asyncio.TimeoutError) as exc:
if isinstance(exc, ConnectionClosed):
# The following close codes are undocumented so I will document them here.
# 1000 - normal closure (obviously)
# 4014 - voice channel has been deleted.
# 4015 - voice server has crashed
if exc.code in (1000, 4015):
_log.info('Disconnecting from voice normally, close code %d.', exc.code)
await self.disconnect()
break
if exc.code == 4014:
_log.info('Disconnected from voice by force... potentially reconnecting.')
successful = await self.potential_reconnect()
if not successful:
_log.info('Reconnect was unsuccessful, disconnecting from voice normally...')
await self.disconnect()
break
else:
continue
if not reconnect:
await self.disconnect()
raise
retry = backoff.delay()
_log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry)
self._connected.clear()
await asyncio.sleep(retry)
await self.voice_disconnect()
try:
await self.connect(reconnect=True, timeout=self.timeout)
except asyncio.TimeoutError:
# at this point we've retried 5 times... let's continue the loop.
_log.warning('Could not connect to voice... Retrying...')
continue
async def disconnect(self, *, force: bool = False) -> None:
"""|coro|
Disconnects this voice client from voice.
"""
if not force and not self.is_connected():
return
self.stop()
self._connected.clear()
await self._connection.disconnect(force=force)
self.cleanup()
try:
if self.ws:
await self.ws.close()
await self.voice_disconnect()
finally:
self.cleanup()
if self.socket:
self.socket.close()
async def move_to(self, channel: Optional[abc.Snowflake]) -> None:
async def move_to(self, channel: Optional[abc.Snowflake], *, timeout: Optional[float] = 30.0) -> None:
"""|coro|
Moves you to a different voice channel.
@ -520,12 +345,21 @@ class VoiceClient(VoiceProtocol):
-----------
channel: Optional[:class:`abc.Snowflake`]
The channel to move to. Must be a voice channel.
timeout: Optional[:class:`float`]
How long to wait for the move to complete.
.. versionadded:: 2.4
Raises
-------
asyncio.TimeoutError
The move did not complete in time, but may still be ongoing.
"""
await self.channel.guild.change_voice_state(channel=channel)
await self._connection.move_to(channel, timeout)
def is_connected(self) -> bool:
"""Indicates if the voice client is connected to voice."""
return self._connected.is_set()
return self._connection.is_connected()
# audio related
@ -564,7 +398,18 @@ class VoiceClient(VoiceProtocol):
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]
def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None) -> None:
def play(
self,
source: AudioSource,
*,
after: Optional[Callable[[Optional[Exception]], Any]] = None,
application: APPLICATION_CTL = 'audio',
bitrate: int = 128,
fec: bool = True,
expected_packet_loss: float = 0.15,
bandwidth: BAND_CTL = 'full',
signal_type: SIGNAL_CTL = 'auto',
) -> None:
"""Plays an :class:`AudioSource`.
The finalizer, ``after`` is called after the source has been exhausted
@ -574,9 +419,15 @@ class VoiceClient(VoiceProtocol):
caught and the audio player is then stopped. If no after callback is
passed, any caught exception will be logged using the library logger.
Extra parameters may be passed to the internal opus encoder if a PCM based
source is used. Otherwise, they are ignored.
.. versionchanged:: 2.0
Instead of writing to ``sys.stderr``, the library's logger is used.
.. versionchanged:: 2.4
Added encoder parameters as keyword arguments.
Parameters
-----------
source: :class:`AudioSource`
@ -585,6 +436,27 @@ class VoiceClient(VoiceProtocol):
The finalizer that is called after the stream is exhausted.
This function must have a single parameter, ``error``, that
denotes an optional exception that was raised during playing.
application: :class:`str`
Configures the encoder's intended application. Can be one of:
``'audio'``, ``'voip'``, ``'lowdelay'``.
Defaults to ``'audio'``.
bitrate: :class:`int`
Configures the bitrate in the encoder. Can be between ``16`` and ``512``.
Defaults to ``128``.
fec: :class:`bool`
Configures the encoder's use of inband forward error correction.
Defaults to ``True``.
expected_packet_loss: :class:`float`
Configures the encoder's expected packet loss percentage. Requires FEC.
Defaults to ``0.15``.
bandwidth: :class:`str`
Configures the encoder's bandpass. Can be one of:
``'narrow'``, ``'medium'``, ``'wide'``, ``'superwide'``, ``'full'``.
Defaults to ``'full'``.
signal_type: :class:`str`
Configures the type of signal being encoded. Can be one of:
``'auto'``, ``'voice'``, ``'music'``.
Defaults to ``'auto'``.
Raises
-------
@ -594,6 +466,8 @@ class VoiceClient(VoiceProtocol):
Source is not a :class:`AudioSource` or after is not a callable.
OpusNotLoaded
Source is not opus encoded and opus is not loaded.
ValueError
An improper value was passed as an encoder parameter.
"""
if not self.is_connected():
@ -605,8 +479,15 @@ class VoiceClient(VoiceProtocol):
if not isinstance(source, AudioSource):
raise TypeError(f'source must be an AudioSource not {source.__class__.__name__}')
if not self.encoder and not source.is_opus():
self.encoder = opus.Encoder()
if not source.is_opus():
self.encoder = opus.Encoder(
application=application,
bitrate=bitrate,
fec=fec,
expected_packet_loss=expected_packet_loss,
bandwidth=bandwidth,
signal_type=signal_type,
)
self._player = AudioPlayer(source, self, after=after)
self._player.start()
@ -651,7 +532,7 @@ class VoiceClient(VoiceProtocol):
if self._player is None:
raise ValueError('Not playing anything.')
self._player._set_source(value)
self._player.set_source(value)
def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None:
"""Sends an audio packet composed of the data.
@ -680,8 +561,8 @@ class VoiceClient(VoiceProtocol):
encoded_data = data
packet = self._get_voice_packet(encoded_data)
try:
self.socket.sendto(packet, (self.endpoint_ip, self.voice_port))
except BlockingIOError:
_log.warning('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp)
self._connection.send_packet(packet)
except OSError:
_log.info('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp)
self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295)

615
discord/voice_state.py

@ -0,0 +1,615 @@
"""
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.
Some documentation to refer to:
- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID.
- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE.
- We pull the session_id from VOICE_STATE_UPDATE.
- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE.
- Then we initiate the voice web socket (vWS) pointing to the endpoint.
- We send opcode 0 with the user_id, server_id, session_id and token using the vWS.
- The vWS sends back opcode 2 with an ssrc, port, modes(array) and heartbeat_interval.
- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE.
- Then we send our IP and port via vWS with opcode 1.
- When that's all done, we receive opcode 4 from the vWS.
- Finally we can transmit data to endpoint:port.
"""
from __future__ import annotations
import select
import socket
import asyncio
import logging
import threading
try:
from asyncio import timeout as atimeout # type: ignore
except ImportError:
from async_timeout import timeout as atimeout # type: ignore
from typing import TYPE_CHECKING, Optional, Dict, List, Callable, Coroutine, Any, Tuple
from .enums import Enum
from .utils import MISSING, sane_wait_for
from .errors import ConnectionClosed
from .backoff import ExponentialBackoff
from .gateway import DiscordVoiceWebSocket
if TYPE_CHECKING:
from . import abc
from .guild import Guild
from .user import ClientUser
from .member import VoiceState
from .voice_client import VoiceClient
from .types.voice import (
GuildVoiceState as GuildVoiceStatePayload,
VoiceServerUpdate as VoiceServerUpdatePayload,
SupportedModes,
)
WebsocketHook = Optional[Callable[[DiscordVoiceWebSocket, Dict[str, Any]], Coroutine[Any, Any, Any]]]
SocketReaderCallback = Callable[[bytes], Any]
__all__ = ('VoiceConnectionState',)
_log = logging.getLogger(__name__)
class SocketReader(threading.Thread):
def __init__(self, state: VoiceConnectionState) -> None:
super().__init__(daemon=True, name=f'voice-socket-reader:{id(self):#x}')
self.state: VoiceConnectionState = state
self._callbacks: List[SocketReaderCallback] = []
self._running = threading.Event()
self._end = threading.Event()
# If we have paused reading due to having no callbacks
self._idle_paused: bool = True
def register(self, callback: SocketReaderCallback) -> None:
self._callbacks.append(callback)
if self._idle_paused:
self._idle_paused = False
self._running.set()
def unregister(self, callback: SocketReaderCallback) -> None:
try:
self._callbacks.remove(callback)
except ValueError:
pass
else:
if not self._callbacks and self._running.is_set():
# If running is not set, we are either explicitly paused and
# should be explicitly resumed, or we are already idle paused
self._idle_paused = True
self._running.clear()
def pause(self) -> None:
self._idle_paused = False
self._running.clear()
def resume(self, *, force: bool = False) -> None:
if self._running.is_set():
return
# Don't resume if there are no callbacks registered
if not force and not self._callbacks:
# We tried to resume but there was nothing to do, so resume when ready
self._idle_paused = True
return
self._idle_paused = False
self._running.set()
def stop(self) -> None:
self._end.set()
self._running.set()
def run(self) -> None:
self._end.clear()
self._running.set()
try:
self._do_run()
except Exception:
_log.exception('Error in %s', self)
finally:
self.stop()
self._running.clear()
self._callbacks.clear()
def _do_run(self) -> None:
while not self._end.is_set():
if not self._running.is_set():
self._running.wait()
continue
# Since this socket is a non blocking socket, select has to be used to wait on it for reading.
try:
readable, _, _ = select.select([self.state.socket], [], [], 30)
except (ValueError, TypeError):
# The socket is either closed or doesn't exist at the moment
continue
if not readable:
continue
try:
data = self.state.socket.recv(2048)
except OSError:
_log.debug('Error reading from socket in %s, this should be safe to ignore', self, exc_info=True)
else:
for cb in self._callbacks:
try:
cb(data)
except Exception:
_log.exception('Error calling %s in %s', cb, self)
class ConnectionFlowState(Enum):
"""Enum representing voice connection flow state."""
# fmt: off
disconnected = 0
set_guild_voice_state = 1
got_voice_state_update = 2
got_voice_server_update = 3
got_both_voice_updates = 4
websocket_connected = 5
got_websocket_ready = 6
got_ip_discovery = 7
connected = 8
# fmt: on
class VoiceConnectionState:
"""Represents the internal state of a voice connection."""
def __init__(self, voice_client: VoiceClient, *, hook: Optional[WebsocketHook] = None) -> None:
self.voice_client = voice_client
self.hook = hook
self.timeout: float = 30.0
self.reconnect: bool = True
self.self_deaf: bool = False
self.self_mute: bool = False
self.token: Optional[str] = None
self.session_id: Optional[str] = None
self.endpoint: Optional[str] = None
self.endpoint_ip: Optional[str] = None
self.server_id: Optional[int] = None
self.ip: Optional[str] = None
self.port: Optional[int] = None
self.voice_port: Optional[int] = None
self.secret_key: List[int] = MISSING
self.ssrc: int = MISSING
self.mode: SupportedModes = MISSING
self.socket: socket.socket = MISSING
self.ws: DiscordVoiceWebSocket = MISSING
self._state: ConnectionFlowState = ConnectionFlowState.disconnected
self._expecting_disconnect: bool = False
self._connected = threading.Event()
self._state_event = asyncio.Event()
self._runner: Optional[asyncio.Task] = None
self._connector: Optional[asyncio.Task] = None
self._socket_reader = SocketReader(self)
self._socket_reader.start()
@property
def state(self) -> ConnectionFlowState:
return self._state
@state.setter
def state(self, state: ConnectionFlowState) -> None:
if state is not self._state:
_log.debug('Connection state changed to %s', state.name)
self._state = state
self._state_event.set()
self._state_event.clear()
if state is ConnectionFlowState.connected:
self._connected.set()
else:
self._connected.clear()
@property
def guild(self) -> Guild:
return self.voice_client.guild
@property
def user(self) -> ClientUser:
return self.voice_client.user
@property
def supported_modes(self) -> Tuple[SupportedModes, ...]:
return self.voice_client.supported_modes
@property
def self_voice_state(self) -> Optional[VoiceState]:
return self.guild.me.voice
async def voice_state_update(self, data: GuildVoiceStatePayload) -> None:
channel_id = data['channel_id']
if channel_id is None:
# If we know we're going to get a voice_state_update where we have no channel due to
# being in the reconnect flow, we ignore it. Otherwise, it probably wasn't from us.
if self._expecting_disconnect:
self._expecting_disconnect = False
else:
_log.debug('We were externally disconnected from voice.')
await self.disconnect()
return
self.session_id = data['session_id']
# we got the event while connecting
if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update):
if self.state is ConnectionFlowState.set_guild_voice_state:
self.state = ConnectionFlowState.got_voice_state_update
else:
self.state = ConnectionFlowState.got_both_voice_updates
return
if self.state is ConnectionFlowState.connected:
self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore
elif self.state is not ConnectionFlowState.disconnected:
if channel_id != self.voice_client.channel.id:
# For some unfortunate reason we were moved during the connection flow
_log.info('Handling channel move while connecting...')
self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore
await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update)
await self.connect(
reconnect=self.reconnect,
timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute,
resume=False,
wait=False,
)
else:
_log.debug('Ignoring unexpected voice_state_update event')
async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
self.token = data['token']
self.server_id = int(data['guild_id'])
endpoint = data.get('endpoint')
if self.token is None or endpoint is None:
_log.warning(
'Awaiting endpoint... This requires waiting. '
'If timeout occurred considering raising the timeout and reconnecting.'
)
return
self.endpoint, _, _ = endpoint.rpartition(':')
if self.endpoint.startswith('wss://'):
# Just in case, strip it off since we're going to add it later
self.endpoint = self.endpoint[6:]
# we got the event while connecting
if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_state_update):
# This gets set after READY is received
self.endpoint_ip = MISSING
self._create_socket()
if self.state is ConnectionFlowState.set_guild_voice_state:
self.state = ConnectionFlowState.got_voice_server_update
else:
self.state = ConnectionFlowState.got_both_voice_updates
elif self.state is ConnectionFlowState.connected:
_log.debug('Voice server update, closing old voice websocket')
await self.ws.close(4014)
self.state = ConnectionFlowState.got_voice_server_update
elif self.state is not ConnectionFlowState.disconnected:
_log.debug('Unexpected server update event, attempting to handle')
await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update)
await self.connect(
reconnect=self.reconnect,
timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute,
resume=False,
wait=False,
)
self._create_socket()
async def connect(
self, *, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool, wait: bool = True
) -> None:
if self._connector:
self._connector.cancel()
self._connector = None
if self._runner:
self._runner.cancel()
self._runner = None
self.timeout = timeout
self.reconnect = reconnect
self._connector = self.voice_client.loop.create_task(
self._wrap_connect(reconnect, timeout, self_deaf, self_mute, resume), name='Voice connector'
)
if wait:
await self._connector
async def _wrap_connect(self, *args: Any) -> None:
try:
await self._connect(*args)
except asyncio.CancelledError:
_log.debug('Cancelling voice connection')
await self.soft_disconnect()
raise
except asyncio.TimeoutError:
_log.info('Timed out connecting to voice')
await self.disconnect()
raise
except Exception:
_log.exception('Error connecting to voice... disconnecting')
await self.disconnect()
raise
async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None:
_log.info('Connecting to voice...')
async with atimeout(timeout):
for i in range(5):
_log.info('Starting voice handshake... (connection attempt %d)', i + 1)
await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute)
# Setting this unnecessarily will break reconnecting
if self.state is ConnectionFlowState.disconnected:
self.state = ConnectionFlowState.set_guild_voice_state
await self._wait_for_state(ConnectionFlowState.got_both_voice_updates)
_log.info('Voice handshake complete. Endpoint found: %s', self.endpoint)
try:
self.ws = await self._connect_websocket(resume)
await self._handshake_websocket()
break
except ConnectionClosed:
if reconnect:
wait = 1 + i * 2.0
_log.exception('Failed to connect to voice... Retrying in %ss...', wait)
await self.disconnect(cleanup=False)
await asyncio.sleep(wait)
continue
else:
await self.disconnect()
raise
_log.info('Voice connection complete.')
if not self._runner:
self._runner = self.voice_client.loop.create_task(self._poll_voice_ws(reconnect), name='Voice websocket poller')
async def disconnect(self, *, force: bool = True, cleanup: bool = True) -> None:
if not force and not self.is_connected():
return
try:
if self.ws:
await self.ws.close()
await self._voice_disconnect()
except Exception:
_log.debug('Ignoring exception disconnecting from voice', exc_info=True)
finally:
self.ip = MISSING
self.port = MISSING
self.state = ConnectionFlowState.disconnected
self._socket_reader.pause()
# Flip the connected event to unlock any waiters
self._connected.set()
self._connected.clear()
if cleanup:
self._socket_reader.stop()
self.voice_client.cleanup()
if self.socket:
self.socket.close()
async def soft_disconnect(self, *, with_state: ConnectionFlowState = ConnectionFlowState.got_both_voice_updates) -> None:
_log.debug('Soft disconnecting from voice')
# Stop the websocket reader because closing the websocket will trigger an unwanted reconnect
if self._runner:
self._runner.cancel()
self._runner = None
try:
if self.ws:
await self.ws.close()
except Exception:
_log.debug('Ignoring exception soft disconnecting from voice', exc_info=True)
finally:
self.ip = MISSING
self.port = MISSING
self.state = with_state
self._socket_reader.pause()
if self.socket:
self.socket.close()
async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[float]) -> None:
if channel is None:
await self.disconnect()
return
previous_state = self.state
# this is only an outgoing ws request
# if it fails, nothing happens and nothing changes (besides self.state)
await self._move_to(channel)
last_state = self.state
try:
await self.wait_async(timeout)
except asyncio.TimeoutError:
_log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id)
if self.state is last_state:
_log.debug('Reverting to previous state %s', previous_state.name)
self.state = previous_state
def wait(self, timeout: Optional[float] = None) -> bool:
return self._connected.wait(timeout)
async def wait_async(self, timeout: Optional[float] = None) -> None:
await self._wait_for_state(ConnectionFlowState.connected, timeout=timeout)
def is_connected(self) -> bool:
return self.state is ConnectionFlowState.connected
def send_packet(self, packet: bytes) -> None:
self.socket.sendall(packet)
def add_socket_listener(self, callback: SocketReaderCallback) -> None:
_log.debug('Registering socket listener callback %s', callback)
self._socket_reader.register(callback)
def remove_socket_listener(self, callback: SocketReaderCallback) -> None:
_log.debug('Unregistering socket listener callback %s', callback)
self._socket_reader.unregister(callback)
async def _wait_for_state(
self, state: ConnectionFlowState, *other_states: ConnectionFlowState, timeout: Optional[float] = None
) -> None:
states = (state, *other_states)
while True:
if self.state in states:
return
await sane_wait_for([self._state_event.wait()], timeout=timeout)
async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False) -> None:
channel = self.voice_client.channel
await channel.guild.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute)
async def _voice_disconnect(self) -> None:
_log.info(
'The voice handshake is being terminated for Channel ID %s (Guild ID %s)',
self.voice_client.channel.id,
self.voice_client.guild.id,
)
self.state = ConnectionFlowState.disconnected
await self.voice_client.channel.guild.change_voice_state(channel=None)
self._expecting_disconnect = True
async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket:
ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook)
self.state = ConnectionFlowState.websocket_connected
return ws
async def _handshake_websocket(self) -> None:
while not self.ip:
await self.ws.poll_event()
self.state = ConnectionFlowState.got_ip_discovery
while self.ws.secret_key is None:
await self.ws.poll_event()
self.state = ConnectionFlowState.connected
def _create_socket(self) -> None:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False)
self._socket_reader.resume()
async def _poll_voice_ws(self, reconnect: bool) -> None:
backoff = ExponentialBackoff()
while True:
try:
await self.ws.poll_event()
except asyncio.CancelledError:
return
except (ConnectionClosed, asyncio.TimeoutError) as exc:
if isinstance(exc, ConnectionClosed):
# The following close codes are undocumented so I will document them here.
# 1000 - normal closure (obviously)
# 4014 - we were externally disconnected (voice channel deleted, we were moved, etc)
# 4015 - voice server has crashed
if exc.code in (1000, 4015):
_log.info('Disconnecting from voice normally, close code %d.', exc.code)
await self.disconnect()
break
if exc.code == 4014:
_log.info('Disconnected from voice by force... potentially reconnecting.')
successful = await self._potential_reconnect()
if not successful:
_log.info('Reconnect was unsuccessful, disconnecting from voice normally...')
await self.disconnect()
break
else:
continue
_log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason')
if not reconnect:
await self.disconnect()
raise
retry = backoff.delay()
_log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry)
await asyncio.sleep(retry)
await self.disconnect(cleanup=False)
try:
await self._connect(
reconnect=reconnect,
timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute,
resume=False,
)
except asyncio.TimeoutError:
# at this point we've retried 5 times... let's continue the loop.
_log.warning('Could not connect to voice... Retrying...')
continue
async def _potential_reconnect(self) -> bool:
try:
await self._wait_for_state(
ConnectionFlowState.got_voice_server_update, ConnectionFlowState.got_both_voice_updates, timeout=self.timeout
)
except asyncio.TimeoutError:
return False
try:
self.ws = await self._connect_websocket(False)
await self._handshake_websocket()
except (ConnectionClosed, asyncio.TimeoutError):
return False
else:
return True
async def _move_to(self, channel: abc.Snowflake) -> None:
await self.voice_client.channel.guild.change_voice_state(channel=channel)
self.state = ConnectionFlowState.set_guild_voice_state

20
docs/_static/style.css

@ -16,6 +16,10 @@ Historically however, thanks to:
box-sizing: border-box;
}
section {
word-break: break-word;
}
/* CSS variables would go here */
:root {
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
@ -109,6 +113,12 @@ Historically however, thanks to:
--attribute-table-entry-hover-text: var(--blue-2);
--attribute-table-badge: var(--grey-7);
--highlighted-text: rgb(252, 233, 103);
--tabs--label-text: var(--main-text);
--tabs--label-text--hover: var(--main-text);
--tabs--label-text--active: var(--blue-1);
--tabs--label-text--active--hover: var(--blue-1);
--tabs--label-border--active: var(--blue-1);
--tabs--label-border--active--hover: var(--blue-1);
}
:root[data-font="serif"] {
@ -216,6 +226,7 @@ body {
display: grid;
min-height: 100%;
grid-auto-rows: min-content auto min-content;
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"s"
"h"
@ -1046,6 +1057,7 @@ code.xref, a code {
span.pre {
padding: 0 2px;
white-space: pre-wrap !important;
}
dl.class {
@ -1211,12 +1223,13 @@ div.code-block-caption {
/* desktop stuff */
@media screen and (min-width: 600px) {
@media screen and (min-width: 768px) {
.grid-item {
max-width: unset;
}
.main-grid {
grid-template-columns: repeat(6, 1fr);
grid-template-areas:
"h h h h h h"
"n n n n n n"
@ -1273,6 +1286,7 @@ div.code-block-caption {
position: sticky;
top: 1em;
max-height: calc(100vh - 2em);
max-width: 100%;
overflow-y: auto;
margin: 1em;
}
@ -1322,6 +1336,10 @@ div.code-block-caption {
"s s s f f f f f f f f f f f f f"
}
#sidebar {
max-width: unset;
}
header > nav {
margin-left: 18.75%;
margin-right: 18.75%;

285
docs/api.rst

@ -496,6 +496,47 @@ Debug
:type payload: Union[:class:`bytes`, :class:`str`]
Entitlements
~~~~~~~~~~~~
.. function:: on_entitlement_create(entitlement)
Called when a user subscribes to a SKU.
.. versionadded:: 2.4
:param entitlement: The entitlement that was created.
:type entitlement: :class:`Entitlement`
.. function:: on_entitlement_update(entitlement)
Called when a user updates their subscription to a SKU. This is usually called when
the user renews or cancels their subscription.
.. versionadded:: 2.4
:param entitlement: The entitlement that was updated.
:type entitlement: :class:`Entitlement`
.. function:: on_entitlement_delete(entitlement)
Called when a users subscription to a SKU is cancelled. This is typically only called when:
- Discord issues a refund for the subscription.
- Discord removes an entitlement from a user.
.. warning::
This event won't be called if the user cancels their subscription manually, instead
:func:`on_entitlement_update` will be called with :attr:`Entitlement.ends_at` set to the end of the
current billing period.
.. versionadded:: 2.4
:param entitlement: The entitlement that was deleted.
:type entitlement: :class:`Entitlement`
Gateway
~~~~~~~~
@ -835,7 +876,7 @@ Members
.. function:: on_member_ban(guild, user)
Called when user gets banned from a :class:`Guild`.
Called when a user gets banned from a :class:`Guild`.
This requires :attr:`Intents.moderation` to be enabled.
@ -1029,6 +1070,12 @@ Reactions
Consider using :func:`on_raw_reaction_add` if you need this and do not otherwise want
to enable the members intent.
.. warning::
This event does not have a way of differentiating whether a reaction is a
burst reaction (also known as "super reaction") or not. If you need this,
consider using :func:`on_raw_reaction_add` instead.
:param reaction: The current state of the reaction.
:type reaction: :class:`Reaction`
:param user: The user who added the reaction.
@ -1051,6 +1098,12 @@ Reactions
Consider using :func:`on_raw_reaction_remove` if you need this and do not want
to enable the members intent.
.. warning::
This event does not have a way of differentiating whether a reaction is a
burst reaction (also known as "super reaction") or not. If you need this,
consider using :func:`on_raw_reaction_remove` instead.
:param reaction: The current state of the reaction.
:type reaction: :class:`Reaction`
:param user: The user whose reaction was removed.
@ -1513,6 +1566,12 @@ of :class:`enum.Enum`.
.. versionadded:: 2.0
.. attribute:: media
A media channel.
.. versionadded:: 2.4
.. class:: MessageType
Specifies the type of :class:`Message`. This is used to denote if a message
@ -2100,6 +2159,11 @@ of :class:`enum.Enum`.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
the :class:`User` or :class:`Object` who got kicked.
When this is the action, the type of :attr:`~AuditLogEntry.extra` is
set to an unspecified proxy object with one attribute:
- ``integration_type``: An optional string that denotes the type of integration that did the action.
When this is the action, :attr:`~AuditLogEntry.changes` is empty.
.. attribute:: member_prune
@ -2160,6 +2224,11 @@ of :class:`enum.Enum`.
When this is the action, the type of :attr:`~AuditLogEntry.target` is
the :class:`Member`, :class:`User`, or :class:`Object` who got the role.
When this is the action, the type of :attr:`~AuditLogEntry.extra` is
set to an unspecified proxy object with one attribute:
- ``integration_type``: An optional string that denotes the type of integration that did the action.
Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.roles`
@ -2799,6 +2868,18 @@ of :class:`enum.Enum`.
.. versionadded:: 2.1
.. attribute:: creator_monetization_request_created
A request to monetize the server was created.
.. versionadded:: 2.4
.. attribute:: creator_monetization_terms_accepted
The terms and conditions for creator monetization were accepted.
.. versionadded:: 2.4
.. attribute:: onboarding_question_create
A guild onboarding prompt was created.
@ -2873,6 +2954,27 @@ of :class:`enum.Enum`.
Represents a member currently in the team.
.. class:: TeamMemberRole
Represents the type of role of a team member retrieved through :func:`Client.application_info`.
.. versionadded:: 2.4
.. attribute:: admin
The team member is an admin. This allows them to invite members to the team, access credentials, edit the application,
and do most things the owner can do. However they cannot do destructive actions.
.. attribute:: developer
The team member is a developer. This allows them to access information, like the client secret or public key.
They can also configure interaction endpoints or reset the bot token. Developers cannot invite anyone to the team
nor can they do destructive actions.
.. attribute:: read_only
The team member is a read-only member. This allows them to access information, but not edit anything.
.. class:: WebhookType
Represents the type of webhook that can be received.
@ -3311,6 +3413,12 @@ of :class:`enum.Enum`.
The rule will trigger when combined number of role and user mentions
is greater than the set limit.
.. attribute:: member_profile
The rule will trigger when a user's profile contains a keyword.
.. versionadded:: 2.4
.. class:: AutoModRuleEventType
Represents the event type of an automod rule.
@ -3321,6 +3429,12 @@ of :class:`enum.Enum`.
The rule will trigger when a message is sent.
.. attribute:: member_update
The rule will trigger when a member's profile is updated.
.. versionadded:: 2.4
.. class:: AutoModRuleActionType
Represents the action type of an automod rule.
@ -3339,6 +3453,12 @@ of :class:`enum.Enum`.
The rule will timeout a user.
.. attribute:: block_member_interactions
Similar to :attr:`timeout`, except the user will be timed out indefinitely.
This will request the user to edit it's profile.
.. versionadded:: 2.4
.. class:: ForumLayoutType
@ -3373,6 +3493,65 @@ of :class:`enum.Enum`.
Sort forum posts by creation time (from most recent to oldest).
.. class:: SelectDefaultValueType
Represents the default value of a select menu.
.. versionadded:: 2.4
.. attribute:: user
The underlying type of the ID is a user.
.. attribute:: role
The underlying type of the ID is a role.
.. attribute:: channel
The underlying type of the ID is a channel or thread.
.. class:: SKUType
Represents the type of a SKU.
.. versionadded:: 2.4
.. attribute:: subscription
The SKU is a recurring subscription.
.. attribute:: subscription_group
The SKU is a system-generated group which is created for each :attr:`SKUType.subscription`.
.. class:: EntitlementType
Represents the type of an entitlement.
.. versionadded:: 2.4
.. attribute:: application_subscription
The entitlement was purchased as an app subscription.
.. class:: EntitlementOwnerType
Represents the type of an entitlement owner.
.. versionadded:: 2.4
.. attribute:: guild
The entitlement owner is a guild.
.. attribute:: user
The entitlement owner is a user.
.. class:: OnboardingPromptType
@ -4024,6 +4203,12 @@ AuditLogDiff
The trigger for the automod rule.
.. note ::
The :attr:`~AutoModTrigger.type` of the trigger may be incorrect.
Some attributes such as :attr:`~AutoModTrigger.keyword_filter`, :attr:`~AutoModTrigger.regex_patterns`,
and :attr:`~AutoModTrigger.allow_list` will only have the added or removed values.
:type: :class:`AutoModTrigger`
.. attribute:: actions
@ -4110,7 +4295,63 @@ AuditLogDiff
See also :attr:`ForumChannel.default_reaction_emoji`
:type: :class:`default_reaction_emoji`
:type: Optional[:class:`PartialEmoji`]
.. 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:: options
@ -4784,6 +5025,22 @@ ShardInfo
.. autoclass:: ShardInfo()
:members:
SKU
~~~~~~~~~~~
.. attributetable:: SKU
.. autoclass:: SKU()
:members:
Entitlement
~~~~~~~~~~~
.. attributetable:: Entitlement
.. autoclass:: Entitlement()
:members:
RawMessageDeleteEvent
~~~~~~~~~~~~~~~~~~~~~~~
@ -5137,6 +5394,30 @@ MemberFlags
.. autoclass:: MemberFlags
:members:
AttachmentFlags
~~~~~~~~~~~~~~~~
.. attributetable:: AttachmentFlags
.. autoclass:: AttachmentFlags
:members:
RoleFlags
~~~~~~~~~~
.. attributetable:: RoleFlags
.. autoclass:: RoleFlags
:members:
SKUFlags
~~~~~~~~~~~
.. attributetable:: SKUFlags
.. autoclass:: SKUFlags()
:members:
ForumTag
~~~~~~~~~

1
docs/conf.py

@ -37,6 +37,7 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinxcontrib_trio',
'sphinx_inline_tabs',
'details',
'exception_hierarchy',
'attributetable',

17
docs/interactions/api.rst

@ -166,6 +166,14 @@ SelectOption
.. autoclass:: SelectOption
:members:
SelectDefaultValue
~~~~~~~~~~~~~~~~~~~
.. attributetable:: SelectDefaultValue
.. autoclass:: SelectDefaultValue
:members:
Choice
~~~~~~~
@ -443,6 +451,15 @@ Item
.. autoclass:: discord.ui.Item
:members:
DynamicItem
~~~~~~~~~~~~
.. attributetable:: discord.ui.DynamicItem
.. autoclass:: discord.ui.DynamicItem
:members:
:inherited-members:
Button
~~~~~~~

3753
docs/locale/ja/LC_MESSAGES/api.po

File diff suppressed because it is too large

64
docs/locale/ja/LC_MESSAGES/discord.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -30,12 +30,12 @@ msgid "Creating a Bot account is a pretty straightforward process."
msgstr "Botのアカウント作成はとても簡単です。"
#: ../../discord.rst:12
#: ../../discord.rst:66
#: ../../discord.rst:61
msgid "Make sure you're logged on to the `Discord website <https://discord.com>`_."
msgstr "`Discordのウェブサイト <https://discord.com>`_ にログインできていることを確認してください。"
#: ../../discord.rst:13
#: ../../discord.rst:67
#: ../../discord.rst:62
msgid "Navigate to the `application page <https://discord.com/developers/applications>`_"
msgstr "`Applicationページ <https://discord.com/developers/applications>`_ に移動します。"
@ -56,22 +56,14 @@ msgid "The new application form filled in."
msgstr "記入された新しいアプリケーションフォーム"
#: ../../discord.rst:24
msgid "Create a Bot User by navigating to the \"Bot\" tab and clicking \"Add Bot\"."
msgstr "「Bot」タブへ移動し、「Add Bot」をクリックしてBotユーザーを作成します。"
#: ../../discord.rst:26
msgid "Click \"Yes, do it!\" to continue."
msgstr "「Yes, do it!」をクリックして続行します。"
#: ../../discord.rst:0
msgid "The Add Bot button."
msgstr "「Add Bot」ボタン"
msgid "Navigate to the \"Bot\" tab to configure it."
msgstr ""
#: ../../discord.rst:30
#: ../../discord.rst:25
msgid "Make sure that **Public Bot** is ticked if you want others to invite your bot."
msgstr "他人にBotの招待を許可する場合には、 **Public Bot** にチェックを入れてください。"
#: ../../discord.rst:32
#: ../../discord.rst:27
msgid "You should also make sure that **Require OAuth2 Code Grant** is unchecked unless you are developing a service that needs it. If you're unsure, then **leave it unchecked**."
msgstr "また、必要なサービスを開発している場合を除いて、 **Require OAuth2 Code Grant** がオフになっていることを確認する必要があります。わからない場合は **チェックを外してください** 。"
@ -79,55 +71,55 @@ msgstr "また、必要なサービスを開発している場合を除いて、
msgid "How the Bot User options should look like for most people."
msgstr "Botユーザーの設定がほとんどの人にとってどのように見えるか"
#: ../../discord.rst:38
#: ../../discord.rst:33
msgid "Copy the token using the \"Copy\" button."
msgstr "「Copy」ボタンを使ってトークンをコピーします。"
#: ../../discord.rst:40
#: ../../discord.rst:35
msgid "**This is not the Client Secret at the General Information page.**"
msgstr "**General InformationページのClient Secretではないので注意してください。**"
#: ../../discord.rst:44
#: ../../discord.rst:39
msgid "It should be worth noting that this token is essentially your bot's password. You should **never** share this with someone else. In doing so, someone can log in to your bot and do malicious things, such as leaving servers, ban all members inside a server, or pinging everyone maliciously."
msgstr "このトークンは、あなたのBotのパスワードと同義であることを覚えておきましょう。誰か他の人とトークンを共有することは絶対に避けてください。トークンがあれば、誰かがあなたのBotにログインし、サーバーから退出したり、サーバー内のすべてのメンバーをBANしたり、すべての人にメンションを送るなどといった悪質な行為を行える様になってしまいます。"
#: ../../discord.rst:49
#: ../../discord.rst:44
msgid "The possibilities are endless, so **do not share this token.**"
msgstr "可能性は無限にあるので、絶対に **トークンを共有しないでください** 。"
#: ../../discord.rst:51
#: ../../discord.rst:46
msgid "If you accidentally leaked your token, click the \"Regenerate\" button as soon as possible. This revokes your old token and re-generates a new one. Now you need to use the new token to login."
msgstr "誤ってトークンを流出させてしまった場合、可能な限り速急に「Regenerate」ボタンをクリックしましょう。これによって古いトークンが無効になり、新しいトークンが再生成されます。今度からは新しいトークンを利用してログインを行う必要があります。"
#: ../../discord.rst:55
#: ../../discord.rst:50
msgid "And that's it. You now have a bot account and you can login with that token."
msgstr "以上です。 これでボットアカウントが作成され、そのトークンでログインできます。"
#: ../../discord.rst:60
#: ../../discord.rst:55
msgid "Inviting Your Bot"
msgstr "Botを招待する"
#: ../../discord.rst:62
#: ../../discord.rst:57
msgid "So you've made a Bot User but it's not actually in any server."
msgstr "Botのユーザーを作成しましたが、現時点ではどのサーバーにも参加していない状態です。"
#: ../../discord.rst:64
#: ../../discord.rst:59
msgid "If you want to invite your bot you must create an invite URL for it."
msgstr "Botを招待したい場合は、そのための招待URLを作成する必要があります。"
#: ../../discord.rst:68
#: ../../discord.rst:63
msgid "Click on your bot's page."
msgstr "Botのページを開きます。"
#: ../../discord.rst:69
msgid "Go to the \"OAuth2\" tab."
msgstr "「OAuth2」タブへ移動します。"
#: ../../discord.rst:64
msgid "Go to the \"OAuth2 > URL Generator\" tab."
msgstr ""
#: ../../discord.rst:0
msgid "How the OAuth2 page should look like."
msgstr "OAuth2ページがどのように見えるか"
#: ../../discord.rst:74
#: ../../discord.rst:69
msgid "Tick the \"bot\" checkbox under \"scopes\"."
msgstr "「scopes」下にある「bot」チェックボックスを選択してください。"
@ -135,15 +127,15 @@ msgstr "「scopes」下にある「bot」チェックボックスを選択して
msgid "The scopes checkbox with \"bot\" ticked."
msgstr "「bot」がチェックされたスコープのチェックボックス"
#: ../../discord.rst:79
#: ../../discord.rst:74
msgid "Tick the permissions required for your bot to function under \"Bot Permissions\"."
msgstr "「Bot Permissions」からBotの機能に必要な権限を選択してください。"
#: ../../discord.rst:81
#: ../../discord.rst:76
msgid "Please be aware of the consequences of requiring your bot to have the \"Administrator\" permission."
msgstr "Botに「管理者」権限を要求させることによる影響は認識しておきましょう。"
#: ../../discord.rst:83
#: ../../discord.rst:78
msgid "Bot owners must have 2FA enabled for certain actions and permissions when added in servers that have Server-Wide 2FA enabled. Check the `2FA support page <https://support.discord.com/hc/en-us/articles/219576828-Setting-up-Two-Factor-Authentication>`_ for more information."
msgstr "二段階認証が有効になっているサーバーにボットを追加する場合、ボットの所有者は特定の動作や権限を与えるために二段階認証を有効化させる必要があります。詳細は `二段階認証のサポートページ <https://support.discord.com/hc/ja/articles/219576828-Setting-up-Two-Factor-Authentication>`_ を参照してください。"
@ -151,15 +143,15 @@ msgstr "二段階認証が有効になっているサーバーにボットを追
msgid "The permission checkboxes with some permissions checked."
msgstr "いくつかの権限にチェックが入った権限のチェックボックス"
#: ../../discord.rst:88
#: ../../discord.rst:83
msgid "Now the resulting URL can be used to add your bot to a server. Copy and paste the URL into your browser, choose a server to invite the bot to, and click \"Authorize\"."
msgstr "結果的に生成されたURLを使ってBotをサーバーに追加することができます。URLをコピーしてブラウザに貼り付け、Botを招待したいサーバーを選択した後、「認証」をクリックしてください。"
#: ../../discord.rst:93
#: ../../discord.rst:88
msgid "The person adding the bot needs \"Manage Server\" permissions to do so."
msgstr "Botを追加する人には「サーバー管理」権限が必要です。"
#: ../../discord.rst:95
#: ../../discord.rst:90
msgid "If you want to generate this URL dynamically at run-time inside your bot and using the :class:`discord.Permissions` interface, you can use :func:`discord.utils.oauth_url`."
msgstr "このURLを実行時に動的に生成したい場合は、 :class:`discord.Permissions` インターフェイスから :func:`discord.utils.oauth_url` を使用できます。"

277
docs/locale/ja/LC_MESSAGES/ext/commands/api.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-29 20:45+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -581,8 +581,8 @@ msgid "A view was not passed."
msgstr "Viewが渡されなかった"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.add_view:16
msgid "The view is not persistent. A persistent view has no timeout and all their components have an explicitly provided custom_id."
msgstr "Viewは永続的ではありません。永続的なViewにはタイムアウトがなく、すべてのコンポーネントには明示的に渡された custom_id があります"
msgid "The view is not persistent or is already finished. A persistent view has no timeout and all their components have an explicitly provided custom_id."
msgstr ""
#: ../../../discord/ext/commands/bot.py:docstring of discord.ext.commands.Bot.allowed_mentions:1
msgid "The allowed mention configuration."
@ -932,14 +932,14 @@ msgid "Retrieves an :term:`asynchronous iterator` that enables receiving your gu
msgstr "Botが所属するGuildを取得できる、 :term:`asynchronous iterator` を取得します。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:5
msgid "Using this, you will only receive :attr:`.Guild.owner`, :attr:`.Guild.icon`, :attr:`.Guild.id`, and :attr:`.Guild.name` per :class:`.Guild`."
msgstr "これを使った場合、各 :class:`Guild` の :attr:`Guild.owner` 、 :attr:`Guild.icon` 、 :attr:`Guild.id` 、 :attr:`Guild.name` のみ取得できます。"
msgid "Using this, you will only receive :attr:`.Guild.owner`, :attr:`.Guild.icon`, :attr:`.Guild.id`, :attr:`.Guild.name`, :attr:`.Guild.approximate_member_count`, and :attr:`.Guild.approximate_presence_count` per :class:`.Guild`."
msgstr ""
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:10
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:11
msgid "This method is an API call. For general usage, consider :attr:`guilds` instead."
msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わりに使用してください。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:13
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:14
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.wait_for:22
#: ../../../discord/ext/commands/core.py:docstring of discord.ext.commands.core.check:37
#: ../../../discord/ext/commands/core.py:docstring of discord.ext.commands.core.check_any:20
@ -947,37 +947,41 @@ msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わ
msgid "Examples"
msgstr "例"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:14
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:15
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.history:7
msgid "Usage ::"
msgstr "使い方 ::"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:19
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:20
msgid "Flattening into a list ::"
msgstr "リストへフラット化 ::"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:24
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:25
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.history:19
msgid "All parameters are optional."
msgstr "すべてのパラメータがオプションです。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:26
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:27
msgid "The number of guilds to retrieve. If ``None``, it retrieves every guild you have access to. Note, however, that this would make it a slow operation. Defaults to ``200``."
msgstr "取得するギルドの数。 ``None`` の場合、Botがアクセスできるギルドすべてを取得します。ただし、これには時間が掛かることに注意してください。デフォルトは200です。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:33
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:34
msgid "The default has been changed to 200."
msgstr "デフォルトが200に変更されました。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:35
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:36
msgid "Retrieves guilds before this date or object. 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."
msgstr "渡された日付、またはギルドより前のギルドを取得します。日付を指定する場合、UTC aware datetimeを利用することを推奨します。naive datetimeである場合、これはローカル時間であるとみなされます。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:39
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:40
msgid "Retrieve guilds after this date or object. 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."
msgstr "渡された日付、またはオブジェクトより後のギルドを取得します。日付を指定する場合、UTC対応の「aware」を利用することを推奨します。日付が「naive」である場合、これは地域時間であるとみなされます。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:44
msgid "Whether to include count information in the guilds. This fills the :attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count` attributes without needing any privileged intents. Defaults to ``True``."
msgstr ""
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:51
msgid "Getting the guilds failed."
msgstr "Guildの取得に失敗した場合。"
@ -989,7 +993,7 @@ msgstr "Guildの取得に失敗した場合。"
msgid "Yields"
msgstr "Yieldする値"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:46
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:53
msgid ":class:`.Guild` -- The guild with the guild data parsed."
msgstr ":class:`.Guild` -- データを解析したGuild。"
@ -2171,7 +2175,7 @@ msgstr "``cls`` で指定されたクラスの構築時に渡すキーワード
#: ../../../discord/ext/commands/core.py:docstring of discord.ext.commands.core.command:21
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_command:29
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_group:9
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_group:12
msgid "If the function is not a coroutine or is already a command."
msgstr "関数がコルーチンでない場合、またはすでにコマンドが登録されている場合。"
@ -2204,7 +2208,7 @@ msgid "Checks and error handlers are dispatched and called as-if they were comma
msgstr "チェックとエラーハンドラは、 :class:`.Command` のようなコマンドであるかのように呼び出されます。つまり、パラメータには :class:`discord.Interaction` ではなく :class:`Context` を取ります。"
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_command:24
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_group:6
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_group:9
msgid "Whether to register the command also as an application command."
msgstr "アプリケーションコマンドとしてもコマンドを登録するかどうか。"
@ -2220,6 +2224,10 @@ msgstr "関数を :class:`.HybridGroup` に変換するデコレータ。"
msgid "This is similar to the :func:`~discord.ext.commands.group` decorator except it creates a hybrid group instead."
msgstr "これは :func:`~discord.ext.commands.group` デコレータに似ていますが、代わりにハイブリッドグループを作成します。"
#: ../../../discord/ext/commands/hybrid.py:docstring of discord.ext.commands.hybrid.hybrid_group:6
msgid "The name to create the group with. By default this uses the function name unchanged."
msgstr ""
#: ../../ext/commands/api.rst:131
msgid "Command"
msgstr "Command"
@ -2925,7 +2933,11 @@ msgstr "コグが削除された際に呼び出される特別なメソッド。
msgid "Subclasses must replace this if they want special unloading behaviour."
msgstr "サブクラスは特別なアンロード動作が必要な場合にこれを置き換えなければなりません。"
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.cog_unload:9
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.cog_unload:7
msgid "Exceptions raised in this method are ignored during extension unloading."
msgstr ""
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.cog_unload:11
msgid "This method can now be a :term:`coroutine`."
msgstr "このメソッドは現在 :term:`coroutine` になりました。"
@ -2947,6 +2959,16 @@ msgstr ":meth:`.Bot.check` チェックとして登録する特別なメソッ
msgid "A special method that registers as a :func:`~discord.ext.commands.check` for every command and subcommand in this cog."
msgstr "このコグのすべてのコマンドとサブコマンドに対して :func:`~discord.ext.commands.check` として登録する特別なメソッド。"
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.interaction_check:1
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.interaction_check:1
msgid "A special method that registers as a :func:`discord.app_commands.check` for every app command and subcommand in this cog."
msgstr ""
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.interaction_check:4
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.interaction_check:4
msgid "This function **can** be a coroutine and must take a sole parameter, ``interaction``, to represent the :class:`~discord.Interaction`."
msgstr ""
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.Cog.cog_command_error:3
msgid "A special method that is called whenever an error is dispatched inside this cog."
msgstr "このコグ内でエラーが発生するたびに呼び出される特別なメソッド。"
@ -3019,8 +3041,8 @@ msgid "Decorators such as :func:`~discord.app_commands.guild_only`, :func:`~disc
msgstr ":func:`~discord.app_commands.guild_only` 、 :func:`~discord.app_commands.guilds` 、 :func:`~discord.app_commands.default_permissions` のようなデコレータはコグの上に使用されている場合、グループに適用されます。"
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.GroupCog:11
msgid "Hybrid commands will also be added to the Group, giving the ability categorize slash commands into groups, while keeping the prefix-style command as a root-level command."
msgstr "グループにもハイブリッドコマンドが追加され、プレフィックス形式のコマンドをルートレベルのコマンドとして保持しながら、スラッシュコマンドをグループに分類できるようになります。"
msgid "Hybrid commands will also be added to the Group, giving the ability to categorize slash commands into groups, while keeping the prefix-style command as a root-level command."
msgstr ""
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.GroupCog:14
msgid "For example:"
@ -3311,7 +3333,7 @@ msgstr "コマンドの最大の幅。"
#: ../../../discord/ext/commands/help.py:docstring of discord.ext.commands.help.DefaultHelpCommand:12
#: ../../../discord/ext/commands/help.py:docstring of discord.ext.commands.help.DefaultHelpCommand:42
#: ../../../discord/ext/commands/help.py:docstring of discord.ext.commands.help.Paginator:25
#: ../../../discord/ext/commands/flags.py:docstring of discord.ext.commands.flags.Flag:42
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.Context.filesize_limit:5
msgid ":class:`int`"
msgstr ":class:`int`"
@ -4423,6 +4445,10 @@ msgstr "「クリーンアップ」されたプレフィックスを返します
msgid "Returns the cog associated with this context's command. None if it does not exist."
msgstr "このコンテキストのコマンドに関連付けられたコグを返します。存在しない場合はNoneを返します。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.Context.filesize_limit:1
msgid "Returns the maximum number of bytes files can have when uploaded to this guild or DM channel associated with this context."
msgstr ""
#: ../../docstring of discord.ext.commands.Context.guild:1
msgid "Returns the guild associated with this context's command. None if not available."
msgstr "このコンテキストのコマンドに関連付けられているギルドを返します。利用できない場合はNoneを返します。"
@ -4496,72 +4522,6 @@ msgstr "ヘルプを表示するエンティティ。"
msgid "The result of the help command, if any."
msgstr "もしあれば、ヘルプコマンドの結果。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:3
msgid "A shortcut method to :meth:`send` to reply to the :class:`~discord.Message` referenced by this context."
msgstr "このコンテキストで参照されている :class:`~discord.Message` に返信するための、 :meth:`send` のショートカットメソッド。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:6
msgid "For interaction based contexts, this is the same as :meth:`send`."
msgstr "インタラクションベースのコンテキストでは、 :meth:`send` と同じです。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:10
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:13
msgid "This function will now raise :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``."
msgstr "この関数は ``InvalidArgument`` の代わりに :exc:`TypeError` または :exc:`ValueError` を発生するようになりました。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:14
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:80
msgid "Sending the message failed."
msgstr "メッセージの送信に失敗しました。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:15
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:81
msgid "You do not have the proper permissions to send the message."
msgstr "メッセージを送信するための適切な権限がありません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:16
msgid "The ``files`` list is not of the appropriate size"
msgstr "``files`` リストの大きさが適切ではありません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:17
msgid "You specified both ``file`` and ``files``."
msgstr "``file`` と ``files`` の両方が指定されています。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:19
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:85
msgid "The message that was sent."
msgstr "送信されたメッセージ。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:20
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.fetch_message:13
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:86
msgid ":class:`~discord.Message`"
msgstr ":class:`~discord.Message`"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:3
msgid "Defers the interaction based contexts."
msgstr "インタラクションの応答を遅らせます。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:5
msgid "This is typically used when the interaction is acknowledged and a secondary action will be done later."
msgstr "これは通常、インタラクションを認識した後、後で他のことを実行する場合に使われます。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:8
msgid "If this isn't an interaction based context then it does nothing."
msgstr "これがインタラクションベースのコンテキストでない場合、何もしません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:10
msgid "Indicates whether the deferred message will eventually be ephemeral."
msgstr "遅れて送信するメッセージが一時的になるかを示します。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:13
msgid "Deferring the interaction failed."
msgstr "インタラクションの遅延に失敗した場合。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:14
msgid "This interaction has already been responded to before."
msgstr "既にインタラクションに応答していた場合。"
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.fetch_message:3
msgid "Retrieves a single :class:`~discord.Message` from the destination."
msgstr "出力先から、単一の :class:`~discord.Message` を取得します。"
@ -4586,6 +4546,12 @@ msgstr "メッセージの取得に失敗した場合。"
msgid "The message asked for."
msgstr "要求されたメッセージ。"
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.fetch_message:13
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:20
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:91
msgid ":class:`~discord.Message`"
msgstr ":class:`~discord.Message`"
#: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.history:1
msgid "Returns an :term:`asynchronous iterator` that enables receiving the destination's message history."
msgstr "出力先のメッセージ履歴を取得する :term:`asynchronous iterator` を返します。"
@ -4654,6 +4620,66 @@ msgstr "現時点でピン留めされているメッセージ。"
msgid "List[:class:`~discord.Message`]"
msgstr "List[:class:`~discord.Message`]"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:3
msgid "A shortcut method to :meth:`send` to reply to the :class:`~discord.Message` referenced by this context."
msgstr "このコンテキストで参照されている :class:`~discord.Message` に返信するための、 :meth:`send` のショートカットメソッド。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:6
msgid "For interaction based contexts, this is the same as :meth:`send`."
msgstr "インタラクションベースのコンテキストでは、 :meth:`send` と同じです。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:10
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:13
msgid "This function will now raise :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``."
msgstr "この関数は ``InvalidArgument`` の代わりに :exc:`TypeError` または :exc:`ValueError` を発生するようになりました。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:14
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:85
msgid "Sending the message failed."
msgstr "メッセージの送信に失敗しました。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:15
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:86
msgid "You do not have the proper permissions to send the message."
msgstr "メッセージを送信するための適切な権限がありません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:16
msgid "The ``files`` list is not of the appropriate size"
msgstr "``files`` リストの大きさが適切ではありません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:17
msgid "You specified both ``file`` and ``files``."
msgstr "``file`` と ``files`` の両方が指定されています。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.reply:19
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:90
msgid "The message that was sent."
msgstr "送信されたメッセージ。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:3
msgid "Defers the interaction based contexts."
msgstr "インタラクションの応答を遅らせます。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:5
msgid "This is typically used when the interaction is acknowledged and a secondary action will be done later."
msgstr "これは通常、インタラクションを認識した後、後で他のことを実行する場合に使われます。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:8
msgid "If this isn't an interaction based context then it does nothing."
msgstr "これがインタラクションベースのコンテキストでない場合、何もしません。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:10
msgid "Indicates whether the deferred message will eventually be ephemeral."
msgstr "遅れて送信するメッセージが一時的になるかを示します。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:13
msgid "Deferring the interaction failed."
msgstr "インタラクションの遅延に失敗した場合。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.defer:14
msgid "This interaction has already been responded to before."
msgstr "既にインタラクションに応答していた場合。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:3
msgid "Sends a message to the destination with the content given."
msgstr "指定された内容のメッセージを出力先に送信します。"
@ -4742,11 +4768,15 @@ msgstr "メッセージの埋め込みを抑制するかどうか。これが ``
msgid "Indicates if the message should only be visible to the user who started the interaction. If a view is sent with an ephemeral message and it has no timeout set then the timeout is set to 15 minutes. **This is only applicable in contexts with an interaction**."
msgstr "メッセージがインタラクションを開始したユーザーだけに表示されるかどうか。もしビューが一時的なメッセージで送信されている、かつタイムアウトが設定されていない場合、タイムアウトは15分に設定されます。 **これはインタラクションベースのコンテキストでのみ適用されます。**"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:82
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:79
msgid "Whether to suppress push and desktop notifications for the message. This will increment the mention counter in the UI, but will not actually send a notification."
msgstr ""
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:87
msgid "The ``files`` list is not of the appropriate size."
msgstr "``files`` リストの大きさが適切でない場合。"
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:83
#: ../../../discord/ext/commands/context.py:docstring of discord.ext.commands.context.Context.send:88
msgid "You specified both ``file`` and ``files``, or you specified both ``embed`` and ``embeds``, or the ``reference`` object is not a :class:`~discord.Message`, :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`."
msgstr "``file`` と ``files`` の両方が指定された場合、 ``embed`` と ``embeds`` の両方が指定された場合、または ``reference`` が :class:`~discord.Message` 、 :class:`~discord.MessageReference` 、 :class:`~discord.PartialMessage` でない場合。"
@ -4864,29 +4894,41 @@ msgstr "メンションで検索"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:9
msgid "Lookup by name#discrim"
msgstr "名前#タグ で検索"
msgid "Lookup by username#discriminator (deprecated)."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:11
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.TextChannelConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.VoiceChannelConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.StageChannelConverter:12
msgid "Lookup by name"
msgstr "名前 で検索"
msgid "Lookup by username#0 (deprecated, only gets users that migrated from their discriminator)."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:12
msgid "Lookup by nickname"
msgstr "ニックネーム で検索"
msgid "Lookup by guild nickname."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:13
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:11
msgid "Lookup by global name."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:14
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:12
msgid "Lookup by user name."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:16
msgid "Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`"
msgstr "一般的な :exc:`.BadArgument` の代わりに :exc:`.MemberNotFound` を発生させます。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:17
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:19
msgid "This converter now lazily fetches members from the gateway and HTTP APIs, optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled."
msgstr "このコンバータは、ゲートウェイやHTTP APIからメンバーを取得でき、 :attr:`.MemberCacheFlags.joined` が有効な場合には結果がキャッシュされるようになりました。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:23
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:21
msgid "Looking up users by discriminator will be removed in a future version due to the removal of discriminators in an API change."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:1
msgid "Converts to a :class:`~discord.User`."
msgstr ":class:`~discord.User` に変換します。"
@ -4895,11 +4937,11 @@ msgstr ":class:`~discord.User` に変換します。"
msgid "All lookups are via the global user cache."
msgstr "すべての検索はグローバルユーザーキャッシュを介して行われます。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:12
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:14
msgid "Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`"
msgstr "一般的な :exc:`.BadArgument` の代わりに :exc:`.UserNotFound` を発生させます。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:15
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:17
msgid "This converter now lazily fetches users from the HTTP APIs if an ID is passed and it's not available in cache."
msgstr "このコンバータは、ID が渡され、キャッシュされていない場合、HTTP API からユーザーを取得するようになりました。"
@ -4958,6 +5000,14 @@ msgstr "名前で検索"
msgid "Converts to a :class:`~discord.TextChannel`."
msgstr ":class:`~discord.TextChannel` に変換します。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.TextChannelConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.VoiceChannelConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.StageChannelConverter:12
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.CategoryChannelConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.ForumChannelConverter:10
msgid "Lookup by name"
msgstr "名前 で検索"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.TextChannelConverter:12
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.VoiceChannelConverter:12
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.CategoryChannelConverter:12
@ -5189,11 +5239,19 @@ msgstr "``Range[int, None, 10]`` は最小値なし、最大値10を意味しま
msgid "``Range[int, 1, 10]`` means the minimum is 1 and the maximum is 10."
msgstr "``Range[int, 1, 10]`` は最小値1、最大値10を意味します。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.Range:12
msgid "``Range[float, 1.0, 5.0]`` means the minimum is 1.0 and the maximum is 5.0."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.Range:13
msgid "``Range[str, 1, 10]`` means the minimum length is 1 and the maximum length is 10."
msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.Range:15
msgid "Inside a :class:`HybridCommand` this functions equivalently to :class:`discord.app_commands.Range`."
msgstr ":class:`HybridCommand` 内では、この関数は :class:`discord.app_commands.Range` と同様に動作します。"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.Range:15
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.Range:17
msgid "If the value cannot be converted to the provided type or is outside the given range, :class:`~.ext.commands.BadArgument` or :class:`~.ext.commands.RangeError` is raised to the appropriate error handlers respectively."
msgstr "もし値が渡された型に変換できず、または指定された範囲外である場合、:class:`~.ext.commands.BadArgument` や :class:`~.ext.commands.RangeError` が適切なエラーハンドラに送出されます。"
@ -5412,6 +5470,11 @@ msgstr "このパラメータの説明。"
msgid "The displayed default in :class:`Command.signature`."
msgstr ":class:`Command.signature` で表示される既定値。"
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.Parameter.displayed_name:1
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:24
msgid "The name that is displayed to the user."
msgstr ""
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.Parameter.get_default:3
msgid "Gets this parameter's default value."
msgstr "このパラメータの既定値を取得します。"
@ -5441,8 +5504,8 @@ msgid "The displayed default in :attr:`Command.signature`."
msgstr ":attr:`Command.signature` で表示される既定値。"
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:1
msgid "param(\\*, converter=..., default=..., description=..., displayed_default=...)"
msgstr "param(\\*, converter=..., default=..., description=..., displayed_default=...)"
msgid "param(\\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...)"
msgstr ""
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:3
msgid "An alias for :func:`parameter`."
@ -5628,6 +5691,10 @@ msgstr "比較が失敗した値の、失敗した順のタプル。"
msgid "Tuple[Any, ``...``]"
msgstr "Tuple[Any, ``...``]"
#: ../../../discord/ext/commands/errors.py:docstring of discord.ext.commands.errors.BadLiteralArgument:28
msgid "The argument's value that failed to be converted. Defaults to an empty string."
msgstr ""
#: ../../../discord/ext/commands/errors.py:docstring of discord.ext.commands.errors.PrivateMessageOnly:1
msgid "Exception raised when an operation does not work outside of private message contexts."
msgstr "プライベートメッセージコンテキスト外で、要求された処理が実行できない場合に発生する例外。"

4
docs/locale/ja/LC_MESSAGES/ext/commands/cogs.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

54
docs/locale/ja/LC_MESSAGES/ext/commands/commands.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -953,103 +953,103 @@ msgstr "もし例外が発生した場合、 :ref:`エラーハンドラ<ext_com
msgid "If you want a more robust error system, you can derive from the exception and raise it instead of returning ``False``:"
msgstr "もし強化されたエラーシステムが必要な場合は、例外を継承し、``False`` を返す代わりに例外を発生させることができます。"
#: ../../ext/commands/commands.rst:1161
#: ../../ext/commands/commands.rst:1162
msgid "Since having a ``guild_only`` decorator is pretty common, it comes built-in via :func:`~ext.commands.guild_only`."
msgstr "``guild_only`` デコレータはよく使われるため、標準で実装されています( :func:`~ext.commands.guild_only` )。"
#: ../../ext/commands/commands.rst:1164
#: ../../ext/commands/commands.rst:1165
msgid "Global Checks"
msgstr "グローバルチェック"
#: ../../ext/commands/commands.rst:1166
#: ../../ext/commands/commands.rst:1167
msgid "Sometimes we want to apply a check to **every** command, not just certain commands. The library supports this as well using the global check concept."
msgstr "**すべての** コマンドにチェックをかけたいこともあるでしょう。そうしたい場合は、ライブラリのグローバルチェックを使うことができます。"
#: ../../ext/commands/commands.rst:1169
#: ../../ext/commands/commands.rst:1170
msgid "Global checks work similarly to regular checks except they are registered with the :meth:`.Bot.check` decorator."
msgstr "グローバルチェックは、 :meth:`.Bot.check` デコレータで登録されることを除き、通常のチェックと同様に動作します。"
#: ../../ext/commands/commands.rst:1171
#: ../../ext/commands/commands.rst:1172
msgid "For example, to block all DMs we could do the following:"
msgstr "例えば、全DMをブロックするには、次の操作を行います。"
#: ../../ext/commands/commands.rst:1181
#: ../../ext/commands/commands.rst:1182
msgid "Be careful on how you write your global checks, as it could also lock you out of your own bot."
msgstr "グローバルチェックを追加するときには注意して下さい。ボットを操作できなくなる可能性があります。"
#: ../../ext/commands/commands.rst:1187
#: ../../ext/commands/commands.rst:1188
msgid "Hybrid Commands"
msgstr "ハイブリッドコマンド"
#: ../../ext/commands/commands.rst:1191
#: ../../ext/commands/commands.rst:1192
msgid ":class:`.commands.HybridCommand` is a command that can be invoked as both a text and a slash command. This allows you to define a command as both slash and text command without writing separate code for both counterparts."
msgstr ":class:`.commands.HybridCommand` は、テキストコマンドとしても、スラッシュコマンドとしても呼び出せるコマンドです。これを使用すれば、別々のコードを書かずにコマンドをスラッシュコマンドとテキストコマンドの両方として定義できます。"
#: ../../ext/commands/commands.rst:1196
#: ../../ext/commands/commands.rst:1197
msgid "In order to define a hybrid command, The command callback should be decorated with :meth:`.Bot.hybrid_command` decorator."
msgstr "ハイブリッドコマンドを定義するには、コマンドコールバックを :meth:`.Bot.hybrid_command` デコレータで装飾しないといけません。"
#: ../../ext/commands/commands.rst:1205
#: ../../ext/commands/commands.rst:1206
msgid "The above command can be invoked as both text and slash command. Note that you have to manually sync your :class:`~app_commands.CommandTree` by calling :class:`~app_commands.CommandTree.sync` in order for slash commands to appear."
msgstr "上のコマンドはテキストコマンドとスラッシュコマンドの両方として実行できます。なお、スラッシュコマンドを表示するには、 :class:`~app_commands.CommandTree.sync` を呼び出して :class:`~app_commands.CommandTree` を手動で同期しないといけません。"
#: ../../ext/commands/commands.rst:1212
#: ../../ext/commands/commands.rst:1213
msgid "You can create hybrid command groups and sub-commands using the :meth:`.Bot.hybrid_group` decorator."
msgstr ":meth:`.Bot.hybrid_group` デコレータを使用して、ハイブリッドコマンドグループとサブコマンドを作成できます。"
#: ../../ext/commands/commands.rst:1225
#: ../../ext/commands/commands.rst:1226
msgid "Due to a Discord limitation, slash command groups cannot be invoked directly so the ``fallback`` parameter allows you to create a sub-command that will be bound to callback of parent group."
msgstr "Discordの制限により、 スラッシュコマンドグループは直接呼び出すことができないため、 ``fallback`` パラメータを使用して、親グループのコールバックを呼び出すサブコマンドを作成できます。"
#: ../../ext/commands/commands.rst:1231
#: ../../ext/commands/commands.rst:1232
msgid "Due to certain limitations on slash commands, some features of text commands are not supported on hybrid commands. You can define a hybrid command as long as it meets the same subset that is supported for slash commands."
msgstr "スラッシュコマンドには制限があるため、ハイブリッドコマンドではテキストコマンドの一部の機能がサポートされていません。スラッシュコマンドでサポートされている機能のみ使用している場合にハイブリッドコマンドを定義できます。"
#: ../../ext/commands/commands.rst:1235
#: ../../ext/commands/commands.rst:1236
msgid "Following are currently **not supported** by hybrid commands:"
msgstr "以下は現時点でハイブリッドコマンドではサポート **されていません**:"
#: ../../ext/commands/commands.rst:1237
#: ../../ext/commands/commands.rst:1238
msgid "Variable number of arguments. e.g. ``*arg: int``"
msgstr "可変長引数。例: ``*arg: int``"
#: ../../ext/commands/commands.rst:1238
#: ../../ext/commands/commands.rst:1239
msgid "Group commands with a depth greater than 1."
msgstr "深さが1より大きいグループコマンド。"
#: ../../ext/commands/commands.rst:1242
#: ../../ext/commands/commands.rst:1243
msgid "Most :class:`typing.Union` types."
msgstr "ほとんどの :class:`typing.Union` 型。"
#: ../../ext/commands/commands.rst:1240
#: ../../ext/commands/commands.rst:1241
msgid "Unions of channel types are allowed"
msgstr "チャンネルの型のユニオン型は使用できます"
#: ../../ext/commands/commands.rst:1241
#: ../../ext/commands/commands.rst:1242
msgid "Unions of user types are allowed"
msgstr "ユーザーの型のユニオン型は使用できます"
#: ../../ext/commands/commands.rst:1242
#: ../../ext/commands/commands.rst:1243
msgid "Unions of user types with roles are allowed"
msgstr "チャンネルの型とロールの型のユニオン型は使用できます"
#: ../../ext/commands/commands.rst:1244
#: ../../ext/commands/commands.rst:1245
msgid "Apart from that, all other features such as converters, checks, autocomplete, flags etc. are supported on hybrid commands. Note that due to a design constraint, decorators related to application commands such as :func:`discord.app_commands.autocomplete` should be placed below the :func:`~ext.commands.hybrid_command` decorator."
msgstr "それ以外の、コンバーター、チェック、オートコンプリート、フラグ、その他はすべてハイブリッドコマンドで利用できます。なお、設計上の制限により、 :func:`discord.app_commands.autocomplete` といったアプリケーションコマンド関連のデコレータは :func:`~ext.commands.hybrid_command` デコレータの下に配置しないといけません。"
#: ../../ext/commands/commands.rst:1248
#: ../../ext/commands/commands.rst:1249
msgid "For convenience and ease in writing code, The :class:`~ext.commands.Context` class implements some behavioural changes for various methods and attributes:"
msgstr "コードを簡単に書くために、 :class:`~ext.commands.Context` クラスのメソッドや属性の動作が変化します:"
#: ../../ext/commands/commands.rst:1251
#: ../../ext/commands/commands.rst:1252
msgid ":attr:`.Context.interaction` can be used to retrieve the slash command interaction."
msgstr ":attr:`.Context.interaction` を用いてスラッシュコマンドのインタラクションを取得できます。"
#: ../../ext/commands/commands.rst:1252
#: ../../ext/commands/commands.rst:1253
msgid "Since interaction can only be responded to once, The :meth:`.Context.send` automatically determines whether to send an interaction response or a followup response."
msgstr "インタラクションは一度しか応答できないため、 :meth:`.Context.send` は、インタラクション応答とフォローアップ応答のどちらを送信するかを自動的に決定します。"
#: ../../ext/commands/commands.rst:1254
#: ../../ext/commands/commands.rst:1255
msgid ":meth:`.Context.defer` defers the interaction response for slash commands but shows typing indicator for text commands."
msgstr ":meth:`.Context.defer` はスラッシュコマンドではインタラクション応答を遅らせ、テキストコマンドでは入力インジケーターを表示します。"

8
docs/locale/ja/LC_MESSAGES/ext/commands/extensions.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -78,6 +78,10 @@ msgid "Although rare, sometimes an extension needs to clean-up or know when it's
msgstr "稀ではありますが、エクステンションにクリーンアップが必要だったり、いつアンロードするかを確認したい場合があります。このために ``setup`` に似たエクステンションがアンロードされるときに呼び出される ``teardown`` というエントリポイントが用意されています。"
#: ../../ext/commands/extensions.rst:57
msgid "Exceptions raised in the ``teardown`` function are ignored, and the extension is still unloaded."
msgstr ""
#: ../../ext/commands/extensions.rst:59
msgid "basic_ext.py"
msgstr "basic_ext.py"

4
docs/locale/ja/LC_MESSAGES/ext/commands/index.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/ext/tasks/index.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/faq.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/index.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/intents.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

138
docs/locale/ja/LC_MESSAGES/interactions/api.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-29 20:45+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -62,7 +62,7 @@ msgid "type"
msgstr "型"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:12
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:36
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:38
#: ../../../discord/message.py:docstring of discord.message.MessageInteraction:23
#: ../../../discord/components.py:docstring of discord.components.SelectMenu:36
#: ../../../discord/components.py:docstring of discord.components.SelectMenu:43
@ -84,7 +84,7 @@ msgid "The guild ID the interaction was sent from."
msgstr "インタラクションが送信されたギルドのID。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:24
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:30
#: ../../../discord/interactions.py:docstring of discord.Interaction.channel_id:3
#: ../../../discord/components.py:docstring of discord.components.TextInput:49
#: ../../../discord/components.py:docstring of discord.components.TextInput:55
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommand:91
@ -92,39 +92,47 @@ msgid "Optional[:class:`int`]"
msgstr "Optional[:class:`int`]"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:28
msgid "The channel ID the interaction was sent from."
msgstr "インタラクションが送信されたチャンネルのID。"
msgid "The channel the interaction was sent from."
msgstr "インタラクションが送信されたチャンネル。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:30
msgid "Note that due to a Discord limitation, if sent from a DM channel :attr:`~DMChannel.recipient` is ``None``."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:32
msgid "Optional[Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`, :class:`Thread`]]"
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:34
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:36
msgid "The application ID that the interaction was for."
msgstr "インタラクションの対象となったアプリケーションのID。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:40
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:42
msgid "The user or member that sent the interaction."
msgstr "インタラクションを送信したユーザーまたはメンバー。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:42
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:44
#: ../../../discord/message.py:docstring of discord.message.MessageInteraction:41
msgid "Union[:class:`User`, :class:`Member`]"
msgstr "Union[:class:`User`, :class:`Member`]"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:46
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:48
msgid "The message that sent this interaction."
msgstr "このインタラクションを送信したメッセージ。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:48
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:50
msgid "This is only available for :attr:`InteractionType.component` interactions."
msgstr "これは :attr:`InteractionType.component` インタラクションの場合にのみ使用できます。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:50
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:52
msgid "Optional[:class:`Message`]"
msgstr "Optional[:class:`Message`]"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:54
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:56
msgid "The token to continue the interaction. These are valid for 15 minutes."
msgstr "インタラクションを続行するのに使うトークン。有効期限は15分です。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:57
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:59
#: ../../docstring of discord.InteractionMessage.clean_content:15
#: ../../../discord/interactions.py:docstring of discord.InteractionMessage.jump_url:3
#: ../../docstring of discord.InteractionMessage.system_content:8
@ -132,43 +140,43 @@ msgstr "インタラクションを続行するのに使うトークン。有効
msgid ":class:`str`"
msgstr ":class:`str`"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:61
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:63
msgid "The raw interaction data."
msgstr "生のインタラクションデータ。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:63
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:83
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:65
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:85
#: ../../../discord/app_commands/commands.py:docstring of discord.app_commands.commands.Command:95
#: ../../../discord/app_commands/commands.py:docstring of discord.app_commands.commands.ContextMenu:80
#: ../../../discord/app_commands/commands.py:docstring of discord.app_commands.commands.Group:105
msgid ":class:`dict`"
msgstr ":class:`dict`"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:67
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:69
msgid "The locale of the user invoking the interaction."
msgstr "インタラクションを呼び出したユーザーのロケール。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:69
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:71
msgid ":class:`Locale`"
msgstr ":class:`Locale`"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:73
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:75
msgid "The preferred locale of the guild the interaction was sent from, if any."
msgstr "インタラクションの送信元のギルドの優先ロケール。もし無ければ ``None`` となります。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:75
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:77
msgid "Optional[:class:`Locale`]"
msgstr "Optional[:class:`Locale`]"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:79
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:81
msgid "A dictionary that can be used to store extraneous data for use during interaction processing. The library will not touch any values or keys within this dictionary."
msgstr "インタラクションの処理中に使用する追加のデータを保管できる辞書型。ライブラリは辞書型の中のキーや値を一切操作しません。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:87
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:89
msgid "Whether the command associated with this interaction failed to execute. This includes checks and execution."
msgstr "このインタラクションに関連付けられたコマンドの実行に失敗したかどうか。これにはチェックとコマンドの実行が含まれます。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:90
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:92
#: ../../../discord/components.py:docstring of discord.components.Button:35
#: ../../../discord/components.py:docstring of discord.components.SelectMenu:55
#: ../../../discord/components.py:docstring of discord.components.TextInput:43
@ -196,17 +204,9 @@ msgstr "インタラクションが送信されたギルド。"
msgid "Optional[:class:`Guild`]"
msgstr "Optional[:class:`Guild`]"
#: ../../docstring of discord.Interaction.channel:1
msgid "The channel the interaction was sent from."
msgstr "インタラクションが送信されたチャンネル。"
#: ../../docstring of discord.Interaction.channel:3
msgid "Note that due to a Discord limitation, DM channels are not resolved since there is no data to complete them. These are :class:`PartialMessageable` instead."
msgstr "Discordの制限により、DMチャンネルは入れるデータがないため解決されないことに注意してください。代わりに :class:`PartialMessageable` があります。"
#: ../../docstring of discord.Interaction.channel:6
msgid "Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]"
msgstr "Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]"
#: ../../../discord/interactions.py:docstring of discord.Interaction.channel_id:1
msgid "The ID of the channel the interaction was sent from."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.Interaction.permissions:1
msgid "The resolved permissions of the member in the channel, including overwrites."
@ -430,7 +430,7 @@ msgid "You specified both ``embed`` and ``embeds``"
msgstr "``embed`` と ``embeds`` の両方を指定した場合。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction.edit_original_response:36
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:39
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:44
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionMessage.edit:35
msgid "The length of ``embeds`` was invalid."
msgstr "``embeds`` の長さが無効だった場合。"
@ -564,7 +564,7 @@ msgstr "インタラクションの遅延に失敗した場合。"
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.defer:25
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.pong:8
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:40
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:45
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.edit_message:35
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_modal:9
msgid "This interaction has already been responded to before."
@ -623,16 +623,20 @@ msgid "Whether to suppress embeds for the message. This sends the message withou
msgstr "メッセージの埋め込みを抑制するかどうか。これが ``True`` に設定されている場合、埋め込みなしでメッセージを送信します。"
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:30
msgid "Whether to suppress push and desktop notifications for the message. This will increment the mention counter in the UI, but will not actually send a notification."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:35
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionMessage.edit:25
msgid "If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, then it is silently ignored."
msgstr "指定すると、これはメッセージを送信したあと削除するまでにバックグラウンドで待機する秒数となります。もし削除が失敗しても、それは静かに無視されます。"
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:37
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:42
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.reply:12
msgid "Sending the message failed."
msgstr "メッセージの送信に失敗した場合。"
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:38
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:43
msgid "You specified both ``embed`` and ``embeds`` or ``file`` and ``files``."
msgstr "``embed`` と ``embeds`` または ``file`` と ``files`` の両方を指定した場合。"
@ -851,34 +855,42 @@ msgid "The name of the thread."
msgstr "スレッドの名前。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:14
msgid "The duration in minutes before a thread is automatically archived for inactivity. If not provided, the channel's default auto archive duration is used."
msgstr "スレッドが非アクティブ時に、自動的にアーカイブされるまでの分単位の長さ。指定しない場合は、チャンネルのデフォルトの自動アーカイブ期間が使用されます。"
msgid "The duration in minutes before a thread is automatically hidden from the channel list. If not provided, the channel's default auto archive duration is used. Must be one of ``60``, ``1440``, ``4320``, or ``10080``, if provided."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:14
msgid "The duration in minutes before a thread is automatically hidden from the channel list. If not provided, the channel's default auto archive duration is used."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:17
msgid "Must be one of ``60``, ``1440``, ``4320``, or ``10080``, if provided."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:19
msgid "Specifies the slowmode rate limit for user in this channel, in seconds. The maximum value possible is ``21600``. By default no slowmode rate limit if this is ``None``."
msgstr "このチャンネルの秒単位での低速モードレート制限。 最大値は ``21600`` です。デフォルトは ``None`` でこの場合は低速モードレート制限が無しとなります。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:21
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:23
msgid "The reason for creating a new thread. Shows up on the audit log."
msgstr "スレッドを作成する理由。監査ログに表示されます。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:24
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:26
msgid "You do not have permissions to create a thread."
msgstr "スレッドを作成する権限を持っていない場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:25
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:27
msgid "Creating the thread failed."
msgstr "スレッドの作成に失敗した場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:26
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:28
msgid "This message does not have guild info attached."
msgstr "メッセージがギルド情報を持っていない場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:28
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:30
msgid "The created thread."
msgstr "作成されたスレッド"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:29
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:31
msgid ":class:`.Thread`"
msgstr ":class:`Thread`"
@ -957,22 +969,22 @@ msgid "Pinning the message failed, probably due to the channel having more t
msgstr "チャンネルにすでに50個ピン留めされたメッセージがあるなどの理由で、メッセージのピン留めに失敗した場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:3
msgid "Publishes this message to your announcement channel."
msgstr "このメッセージをアナウンスチャンネルに公開します。"
msgid "Publishes this message to the channel's followers."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:5
msgid "You must have :attr:`~Permissions.send_messages` to do this."
msgstr "これを行うには、 :attr:`~Permissions.send_messages` が必要です。"
msgid "The message must have been sent in a news channel. You must have :attr:`~Permissions.send_messages` to do this."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:7
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:8
msgid "If the message is not your own then :attr:`~Permissions.manage_messages` is also needed."
msgstr "自身のメッセージ以外の場合は :attr:`~Permissions.manage_messages` も必要です。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:10
msgid "You do not have the proper permissions to publish this message."
msgstr "メッセージを公開する適切な権限がない場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:11
msgid "You do not have the proper permissions to publish this message or the channel is not a news channel."
msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:12
msgid "Publishing the message failed."
msgstr "メッセージの公開に失敗した場合。"
@ -1843,8 +1855,8 @@ msgid "The user's ID that archived this thread."
msgstr "このスレッドをアーカイブしたユーザーのID。"
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommandThread:87
msgid "The duration in minutes until the thread is automatically archived due to inactivity. Usually a value of 60, 1440, 4320 and 10080."
msgstr "非アクティブのスレッドが自動的にアーカイブされるまでの分数です。通常は60、1440、4320、10080の値を設定します。"
msgid "The duration in minutes until the thread is automatically hidden from the channel list. Usually a value of 60, 1440, 4320 and 10080."
msgstr ""
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommandThread:94
msgid "An aware timestamp of when the thread's archived status was last updated in UTC."
@ -2450,7 +2462,7 @@ msgid "This object must be inherited to create a UI within Discord."
msgstr "Discord内でUIを作成するには、このオブジェクトを継承する必要があります。"
#: ../../../discord/ui/view.py:docstring of discord.ui.view.View:7
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:22
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:23
msgid "Timeout in seconds from last interaction with the UI before no longer accepting input. If ``None`` then there is no timeout."
msgstr "UIの最後のインタラクションから起算した、入力を受け付けなくなるまでの秒単位のタイムアウト。 ``None`` の場合タイムアウトはありません。"
@ -2677,19 +2689,19 @@ msgstr "Discord内でモーダルポップアップウィンドウを作成す
msgid "Examples"
msgstr "例"
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:20
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:21
msgid "The title of the modal. Can only be up to 45 characters."
msgstr "モーダルのタイトル。最大45文字までです。"
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:25
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:26
msgid "The ID of the modal that gets received during an interaction. If not given then one is generated for you. Can only be up to 100 characters."
msgstr "インタラクション中に受け取るモーダルID。 指定されていない場合は、自動で生成されます。最大 100 文字までしか使用できません。"
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:32
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:33
msgid "The title of the modal."
msgstr "モーダルのタイトル。"
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:38
#: ../../../discord/ui/modal.py:docstring of discord.ui.modal.Modal:39
msgid "The ID of the modal that gets received during an interaction."
msgstr "インタラクション中に受け取るモーダルID。"

14
docs/locale/ja/LC_MESSAGES/intro.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
@ -109,15 +109,19 @@ msgstr "いつものようにpipインストールを実行します。"
msgid "Congratulations. You now have a virtual environment all set up."
msgstr "おめでとうございます。これで仮想環境のセットアップができました。"
#: ../../intro.rst:92
#: ../../intro.rst:93
msgid "Scripts executed with ``py -3`` will ignore any currently active virtual environment, as the ``-3`` specifies a global scope."
msgstr ""
#: ../../intro.rst:97
msgid "Basic Concepts"
msgstr "基本概念"
#: ../../intro.rst:94
#: ../../intro.rst:99
msgid "discord.py revolves around the concept of :ref:`events <discord-api-events>`. An event is something you listen to and then respond to. For example, when a message happens, you will receive an event about it that you can respond to."
msgstr "discord.pyは :ref:`イベント <discord-api-events>` の概念を中心としています。イベントは何かを受け取り、それに対する応答を行います。例えば、メッセージが発生すると、メッセージの発生に関連するイベントを受け取り、そのイベントに対して応答を返すことができます。"
#: ../../intro.rst:98
#: ../../intro.rst:103
msgid "A quick example to showcase how events work:"
msgstr "以下はイベントの仕組みを紹介する簡単な例です。"

4
docs/locale/ja/LC_MESSAGES/logging.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

608
docs/locale/ja/LC_MESSAGES/migrating.po

File diff suppressed because it is too large

4
docs/locale/ja/LC_MESSAGES/migrating_to_async.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/migrating_to_v1.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/quickstart.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/sphinx.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

4
docs/locale/ja/LC_MESSAGES/version_guarantees.po

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n"
"POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"

2002
docs/locale/ja/LC_MESSAGES/whats_new.po

File diff suppressed because it is too large

6
docs/logging.rst

@ -43,6 +43,12 @@ Likewise, configuring the log level to ``logging.DEBUG`` is also possible:
This is recommended, especially at verbose levels such as ``DEBUG``, as there are a lot of events logged and it would clog the stderr of your program.
If you want the logging configuration the library provides to affect all loggers rather than just the ``discord`` logger, you can pass ``root_logger=True`` inside :meth:`Client.run`:
.. code-block:: python3
client.run(token, log_handler=handler, root_logger=True)
If you want to setup logging using the library provided configuration without using :meth:`Client.run`, you can use :func:`discord.utils.setup_logging`:
.. code-block:: python3

17
docs/whats_new.rst

@ -11,6 +11,23 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed
in specific versions.
.. _vp2p3p2:
v2.3.2
-------
Bug Fixes
~~~~~~~~~~
- Fix the ``name`` parameter not being respected when sending a :class:`CustomActivity`.
- Fix :attr:`Intents.emoji` and :attr:`Intents.emojis_and_stickers` having swapped alias values (:issue:`9471`).
- Fix ``NameError`` when using :meth:`abc.GuildChannel.create_invite` (:issue:`9505`).
- Fix crash when disconnecting during the middle of a ``HELLO`` packet when using :class:`AutoShardedClient`.
- Fix overly eager escape behaviour for lists and header markdown in :func:`utils.escape_markdown` (:issue:`9516`).
- Fix voice websocket not being closed before being replaced by a new one (:issue:`9518`).
- |commands| Fix the wrong :meth:`~ext.commands.HelpCommand.on_help_command_error` being called when ejected from a cog.
- |commands| Fix ``=None`` being displayed in :attr:`~ext.commands.Command.signature`.
.. _vp2p3p1:
v2.3.1

15
examples/advanced_startup.py

@ -1,3 +1,5 @@
# This example requires the 'message_content' privileged intent to function, however your own bot might not.
# This example covers advanced startup options and uses some real world examples for why you may need them.
import asyncio
@ -88,9 +90,16 @@ async def main():
# 2. We become responsible for starting the bot.
exts = ['general', 'mod', 'dice']
async with CustomBot(commands.when_mentioned, db_pool=pool, web_client=our_client, initial_extensions=exts) as bot:
await bot.start(os.getenv('TOKEN', ''))
intents = discord.Intents.default()
intents.message_content = True
async with CustomBot(
commands.when_mentioned,
db_pool=pool,
web_client=our_client,
initial_extensions=exts,
intents=intents,
) as bot:
await bot.start('token')
# For most use cases, after defining what needs to run, we can just tell asyncio to run it:

98
examples/views/dynamic_counter.py

@ -0,0 +1,98 @@
from __future__ import annotations
from discord.ext import commands
import discord
import re
# Complicated use cases for persistent views can be difficult to achieve when dealing
# with state changes or dynamic items. In order to facilitate these complicated use cases,
# the library provides DynamicItem which allows you to define an item backed by a regular
# expression that can parse state out of the custom_id.
# The following example showcases a dynamic item that implements a counter.
# The `template` class parameter is used to give the library a regular expression to parse
# the custom_id. In this case we're parsing out custom_id in the form of e.g.
# `counter:5:user:80088516616269824` where the first number is the current count and the
# second number is the user ID who owns the button.
# Note that custom_ids can only be up to 100 characters long.
class DynamicCounter(
discord.ui.DynamicItem[discord.ui.Button],
template=r'counter:(?P<count>[0-9]+):user:(?P<id>[0-9]+)',
):
def __init__(self, user_id: int, count: int = 0) -> None:
self.user_id: int = user_id
self.count: int = count
super().__init__(
discord.ui.Button(
label=f'Total: {count}',
style=self.style,
custom_id=f'counter:{count}:user:{user_id}',
emoji='\N{THUMBS UP SIGN}',
)
)
# We want the style of the button to be dynamic depending on the count.
@property
def style(self) -> discord.ButtonStyle:
if self.count < 10:
return discord.ButtonStyle.grey
if self.count < 15:
return discord.ButtonStyle.red
if self.count < 20:
return discord.ButtonStyle.blurple
return discord.ButtonStyle.green
# This method actually extracts the information from the custom ID and creates the item.
@classmethod
async def from_custom_id(cls, interaction: discord.Interaction, item: discord.ui.Button, match: re.Match[str], /):
count = int(match['count'])
user_id = int(match['id'])
return cls(user_id, count=count)
# We want to ensure that our button is only called by the user who created it.
async def interaction_check(self, interaction: discord.Interaction) -> bool:
return interaction.user.id == self.user_id
async def callback(self, interaction: discord.Interaction) -> None:
# When the button is invoked, we want to increase the count and update the button's
# styling and label.
# In order to actually persist these changes we need to also update the custom_id
# to match the new information.
# Note that the custom ID *must* match the template.
self.count += 1
self.item.label = f'Total: {self.count}'
self.custom_id = f'counter:{self.count}:user:{self.user_id}'
self.item.style = self.style
# In here, self.view is the view given by the interaction's message.
# It cannot be a custom subclass due to limitations.
await interaction.response.edit_message(view=self.view)
class DynamicCounterBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
super().__init__(command_prefix=commands.when_mentioned, intents=intents)
async def setup_hook(self) -> None:
# For dynamic items, we must register the classes instead of the views.
self.add_dynamic_items(DynamicCounter)
async def on_ready(self):
print(f'Logged in as {self.user} (ID: {self.user.id})')
print('------')
bot = DynamicCounterBot()
@bot.command()
async def counter(ctx: commands.Context):
"""Starts a dynamic counter."""
view = discord.ui.View(timeout=None)
view.add_item(DynamicCounter(ctx.author.id))
await ctx.send('Here is your very own button!', view=view)
bot.run('token')

45
examples/views/persistent.py

@ -1,7 +1,9 @@
# This example requires the 'message_content' privileged intent to function.
from __future__ import annotations
from discord.ext import commands
import discord
import re
# Define a simple View that persists between bot restarts
@ -29,6 +31,38 @@ class PersistentView(discord.ui.View):
await interaction.response.send_message('This is grey.', ephemeral=True)
# More complicated cases might require parsing state out from the custom_id instead.
# For this use case, the library provides a `DynamicItem` to make this easier.
# The same constraints as above apply to this too.
# For this example, the `template` class parameter is used to give the library a regular
# expression to parse the custom_id with.
# These custom IDs will be in the form of e.g. `button:user:80088516616269824`.
class DynamicButton(discord.ui.DynamicItem[discord.ui.Button], template=r'button:user:(?P<id>[0-9]+)'):
def __init__(self, user_id: int) -> None:
super().__init__(
discord.ui.Button(
label='Do Thing',
style=discord.ButtonStyle.blurple,
custom_id=f'button:user:{user_id}',
emoji='\N{THUMBS UP SIGN}',
)
)
self.user_id: int = user_id
# This is called when the button is clicked and the custom_id matches the template.
@classmethod
async def from_custom_id(cls, interaction: discord.Interaction, item: discord.ui.Button, match: re.Match[str], /):
user_id = int(match['id'])
return cls(user_id)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
# Only allow the user who created the button to interact with it.
return interaction.user.id == self.user_id
async def callback(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message('This is your very own button!', ephemeral=True)
class PersistentViewBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
@ -43,6 +77,8 @@ class PersistentViewBot(commands.Bot):
# If you have the message_id you can also pass it as a keyword argument, but for this example
# we don't have one.
self.add_view(PersistentView())
# For dynamic items, we must register the classes instead of the views.
self.add_dynamic_items(DynamicButton)
async def on_ready(self):
print(f'Logged in as {self.user} (ID: {self.user.id})')
@ -63,4 +99,13 @@ async def prepare(ctx: commands.Context):
await ctx.send("What's your favourite colour?", view=PersistentView())
@bot.command()
async def dynamic_button(ctx: commands.Context):
"""Starts a dynamic button."""
view = discord.ui.View(timeout=None)
view.add_item(DynamicButton(ctx.author.id))
await ctx.send('Here is your very own button!', view=view)
bot.run('token')

1
requirements.txt

@ -1 +1,2 @@
aiohttp>=3.7.4,<4
async-timeout>=4.0,<5.0; python_version<"3.11"

3
setup.py

@ -39,6 +39,7 @@ extras_require = {
'sphinxcontrib_trio==1.1.2',
'sphinxcontrib-websupport',
'typing-extensions>=4.3,<5',
'sphinx-inline-tabs',
],
'speed': [
'orjson>=3.5.4',
@ -53,6 +54,7 @@ extras_require = {
'pytest-cov',
'pytest-mock',
'typing-extensions>=4.3,<5',
'tzdata; sys_platform == "win32"',
],
}
@ -93,6 +95,7 @@ setup(
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Internet',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',

110
tests/test_app_commands_group.py

@ -397,3 +397,113 @@ def test_cog_group_with_custom_state_issue9383():
assert cog.inner.my_command.parent is cog.inner
assert cog.my_inner_command.parent is cog.inner
assert cog.my_inner_command.binding is cog
def test_cog_hybrid_group_manual_command():
class MyCog(commands.Cog):
@commands.hybrid_group()
async def first(self, ctx: commands.Context) -> None:
...
@first.command(name='both')
async def second_both(self, ctx: commands.Context) -> None:
...
@first.app_command.command(name='second')
async def second_app(self, interaction: discord.Interaction) -> None:
...
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
cog = MyCog()
tree.add_command(cog.first.app_command)
assert cog.first is not MyCog.first
assert cog.second_both is not MyCog.second_both
assert cog.second_app is not MyCog.second_app
assert cog.first.parent is None
assert cog.second_both.parent is cog.first
assert cog.second_app.parent is cog.first.app_command
assert cog.second_app.binding is cog
assert tree.get_command('first') is cog.first.app_command
first = tree.get_command('first')
assert isinstance(first, app_commands.Group)
both = first.get_command('both')
assert isinstance(both, app_commands.Command)
assert both.parent is first
assert both.binding is cog
second = first.get_command('second')
assert isinstance(second, app_commands.Command)
assert second.parent is first
assert second.binding is cog
def test_cog_hybrid_group_manual_nested_command():
class MyCog(commands.Cog):
@commands.hybrid_group()
async def first(self, ctx: commands.Context) -> None:
pass
@first.group()
async def second(self, ctx: commands.Context) -> None:
pass
@second.app_command.command()
async def third(self, interaction: discord.Interaction) -> None:
pass
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
cog = MyCog()
tree.add_command(cog.first.app_command)
assert cog.first is not MyCog.first
assert cog.second is not MyCog.second
assert cog.third is not MyCog.third
assert cog.first.parent is None
assert cog.second.parent is cog.first
assert cog.third.parent is cog.second.app_command
assert cog.third.binding is cog
first = tree.get_command('first')
assert isinstance(first, app_commands.Group)
second = first.get_command('second')
assert isinstance(second, app_commands.Group)
third = second.get_command('third')
assert isinstance(third, app_commands.Command)
assert third.parent is second
assert third.binding is cog
def test_cog_hybrid_group_wrapped_instance():
class MyCog(commands.Cog):
@commands.hybrid_group(fallback='fallback')
async def first(self, ctx: commands.Context) -> None:
pass
@first.command()
async def second(self, ctx: commands.Context) -> None:
pass
@first.group()
async def nested(self, ctx: commands.Context) -> None:
pass
@nested.app_command.command()
async def child(self, interaction: discord.Interaction) -> None:
pass
cog = MyCog()
fallback = cog.first.app_command.get_command('fallback')
assert fallback is not None
assert getattr(fallback, 'wrapped', None) is cog.first
assert fallback.parent is cog.first.app_command
assert cog.second.app_command is not None
assert cog.second.app_command.wrapped is cog.second

Loading…
Cancel
Save