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

2
.github/workflows/lint.yml

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

1
discord/__init__.py

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

2
discord/__main__.py

@ -157,7 +157,7 @@ _cog_extras = '''
async def cog_command_error(self, ctx, error): async def cog_command_error(self, ctx, error):
# error handling to every command in here # error handling to every command in here
pass pass
async def cog_app_command_error(self, interaction, error): async def cog_app_command_error(self, interaction, error):
# error handling to every application command in here # error handling to every application command in here
pass pass

9
discord/abc.py

@ -48,7 +48,7 @@ from typing import (
from .object import OLDEST_OBJECT, Object from .object import OLDEST_OBJECT, Object
from .context_managers import Typing from .context_managers import Typing
from .enums import ChannelType from .enums import ChannelType, InviteTarget
from .errors import ClientException from .errors import ClientException
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .permissions import PermissionOverwrite, Permissions from .permissions import PermissionOverwrite, Permissions
@ -93,7 +93,6 @@ if TYPE_CHECKING:
StageChannel, StageChannel,
) )
from .threads import Thread from .threads import Thread
from .enums import InviteTarget
from .ui.view import View from .ui.view import View
from .types.channel import ( from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload, PermissionOverwrite as PermissionOverwritePayload,
@ -1246,6 +1245,8 @@ class GuildChannel:
:class:`~discord.Invite` :class:`~discord.Invite`
The invite that was created. 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( data = await self._state.http.create_invite(
self.id, self.id,
@ -1841,7 +1842,7 @@ class Connectable(Protocol):
async def connect( async def connect(
self, self,
*, *,
timeout: float = 60.0, timeout: float = 30.0,
reconnect: bool = True, reconnect: bool = True,
cls: Callable[[Client, Connectable], T] = VoiceClient, cls: Callable[[Client, Connectable], T] = VoiceClient,
self_deaf: bool = False, self_deaf: bool = False,
@ -1857,7 +1858,7 @@ class Connectable(Protocol):
Parameters Parameters
----------- -----------
timeout: :class:`float` 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` reconnect: :class:`bool`
Whether the bot should automatically attempt Whether the bot should automatically attempt
a reconnect if a part of the handshake fails a reconnect if a part of the handshake fails

6
discord/activity.py

@ -732,10 +732,12 @@ class CustomActivity(BaseActivity):
__slots__ = ('name', 'emoji', 'state') __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) super().__init__(**extra)
self.name: Optional[str] = name 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': if self.name == 'Custom Status':
self.name = self.state 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: if self.binding is not None:
check: Optional[Check] = getattr(self.binding, 'interaction_check', None) check: Optional[Check] = getattr(self.binding, 'interaction_check', None)
if check: if check:
ret = await maybe_coroutine(check, interaction) # type: ignore # Probable pyright bug ret = await maybe_coroutine(check, interaction)
if not ret: if not ret:
return False return False
@ -1548,6 +1548,9 @@ class Group:
if not self.description: if not self.description:
raise TypeError('groups must have a 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.parent: Optional[Group] = parent
self.module: Optional[str] self.module: Optional[str]
if cls.__discord_app_commands_has_module__: 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 ..enums import AppCommandOptionType, AppCommandType, Locale
from ..errors import DiscordException, HTTPException, _flatten_error_dict from ..errors import DiscordException, HTTPException, _flatten_error_dict
from ..utils import _human_join
__all__ = ( __all__ = (
'AppCommandError', 'AppCommandError',
@ -242,13 +243,7 @@ class MissingAnyRole(CheckFailure):
def __init__(self, missing_roles: SnowflakeList) -> None: def __init__(self, missing_roles: SnowflakeList) -> None:
self.missing_roles: SnowflakeList = missing_roles self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles] fmt = _human_join([f"'{role}'" for role in missing_roles])
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
message = f'You are missing at least one of the required roles: {fmt}' message = f'You are missing at least one of the required roles: {fmt}'
super().__init__(message) super().__init__(message)
@ -271,11 +266,7 @@ class MissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
fmt = _human_join(missing, final='and')
if len(missing) > 2:
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = f'You are missing {fmt} permission(s) to run this command.' message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args) super().__init__(message, *args)
@ -298,11 +289,7 @@ class BotMissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
fmt = _human_join(missing, final='and')
if len(missing) > 2:
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = f'Bot requires {fmt} permission(s) to run this command.' message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args) 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})'] messages = [f'Failed to upload commands to Discord (HTTP status {self.status}, error code {self.code})']
if self._errors: if self._errors:
for index, inner in self._errors.items(): # Handle case where the errors dict has no actual chain such as APPLICATION_COMMAND_TOO_LARGE
_get_command_error(index, inner, commands, messages) 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 # Equivalent to super().__init__(...) but skips other constructors
self.args = ('\n'.join(messages),) self.args = ('\n'.join(messages),)

7
discord/app_commands/transformers.py

@ -177,8 +177,7 @@ class CommandParameter:
return choice return choice
try: try:
# ParamSpec doesn't understand that transform is a callable since it's unbound return await maybe_coroutine(self._annotation.transform, interaction, value)
return await maybe_coroutine(self._annotation.transform, interaction, value) # type: ignore
except AppCommandError: except AppCommandError:
raise raise
except Exception as e: except Exception as e:
@ -526,7 +525,7 @@ else:
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
def __class_getitem__(cls, items) -> _TransformMetadata: def __class_getitem__(cls, items) -> Transformer:
if not isinstance(items, tuple): if not isinstance(items, tuple):
raise TypeError(f'expected tuple for arguments, received {items.__class__.__name__} instead') 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) 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): if not isinstance(obj, tuple):
raise TypeError(f'expected tuple for arguments, received {obj.__class__.__name__} instead') 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: 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.location: _L = location
self.data: _D = data 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) await command._invoke_autocomplete(interaction, focused, namespace)
except Exception: except Exception:
# Suppress exception since it can't be handled anyway. # Suppress exception since it can't be handled anyway.
pass _log.exception('Ignoring exception in autocomplete for %r', command.qualified_name)
return return

133
discord/audit_logs.py

@ -33,7 +33,7 @@ from .invite import Invite
from .mixins import Hashable from .mixins import Hashable
from .object import Object from .object import Object
from .permissions import PermissionOverwrite, Permissions from .permissions import PermissionOverwrite, Permissions
from .automod import AutoModTrigger, AutoModRuleAction, AutoModPresets, AutoModRule from .automod import AutoModTrigger, AutoModRuleAction, AutoModRule
from .role import Role from .role import Role
from .emoji import Emoji from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
@ -62,6 +62,7 @@ if TYPE_CHECKING:
from .types.audit_log import ( from .types.audit_log import (
AuditLogChange as AuditLogChangePayload, AuditLogChange as AuditLogChangePayload,
AuditLogEntry as AuditLogEntryPayload, AuditLogEntry as AuditLogEntryPayload,
_AuditLogChange_TriggerMetadata as AuditLogChangeTriggerMetadataPayload,
) )
from .types.channel import ( from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload, PermissionOverwrite as PermissionOverwritePayload,
@ -72,7 +73,7 @@ if TYPE_CHECKING:
from .types.role import Role as RolePayload from .types.role import Role as RolePayload
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
from .types.command import ApplicationCommandPermissions 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 .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload
from .user import User from .user import User
from .app_commands import AppCommand from .app_commands import AppCommand
@ -232,39 +233,6 @@ def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]
return _transform 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]: def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAction]) -> List[AutoModRuleAction]:
return [AutoModRuleAction.from_data(action) for action in data] return [AutoModRuleAction.from_data(action) for action in data]
@ -380,7 +348,6 @@ class AuditLogChanges:
'image_hash': ('cover_image', _transform_cover_image), 'image_hash': ('cover_image', _transform_cover_image),
'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)), 'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)),
'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)), 'event_type': (None, _enum_transformer(enums.AutoModRuleEventType)),
'trigger_metadata': ('trigger', _transform_automod_trigger_metadata),
'actions': (None, _transform_automod_actions), 'actions': (None, _transform_automod_actions),
'exempt_channels': (None, _transform_channels_or_threads), 'exempt_channels': (None, _transform_channels_or_threads),
'exempt_roles': (None, _transform_roles), '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 self._handle_role(self.after, self.before, entry, elem['new_value']) # type: ignore # new_value is a list of roles in this case
continue 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: try:
key, transformer = self.TRANSFORMERS[attr] key, transformer = self.TRANSFORMERS[attr]
except (ValueError, KeyError): except (ValueError, KeyError):
@ -505,6 +487,76 @@ class AuditLogChanges:
guild = entry.guild guild = entry.guild
diff.app_command_permissions.append(AppCommandPermissions(data=data, guild=guild, state=state)) 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: class _AuditLogProxy:
def __init__(self, **kwargs: Any) -> None: def __init__(self, **kwargs: Any) -> None:
@ -545,6 +597,10 @@ class _AuditLogProxyAutoModAction(_AuditLogProxy):
channel: Optional[Union[abc.GuildChannel, Thread]] channel: Optional[Union[abc.GuildChannel, Thread]]
class _AuditLogProxyMemberKickOrMemberRoleUpdate(_AuditLogProxy):
integration_type: Optional[str]
class AuditLogEntry(Hashable): class AuditLogEntry(Hashable):
r"""Represents an Audit Log entry. r"""Represents an Audit Log entry.
@ -631,6 +687,7 @@ class AuditLogEntry(Hashable):
_AuditLogProxyStageInstanceAction, _AuditLogProxyStageInstanceAction,
_AuditLogProxyMessageBulkDelete, _AuditLogProxyMessageBulkDelete,
_AuditLogProxyAutoModAction, _AuditLogProxyAutoModAction,
_AuditLogProxyMemberKickOrMemberRoleUpdate,
Member, User, None, PartialIntegration, Member, User, None, PartialIntegration,
Role, Object Role, Object
] = None ] = None
@ -655,6 +712,10 @@ class AuditLogEntry(Hashable):
elif self.action is enums.AuditLogAction.message_bulk_delete: elif self.action is enums.AuditLogAction.message_bulk_delete:
# The bulk message delete action has the number of messages deleted # The bulk message delete action has the number of messages deleted
self.extra = _AuditLogProxyMessageBulkDelete(count=int(extra['count'])) 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'): elif self.action.name.endswith('pin'):
# the pin actions have a dict with some information # the pin actions have a dict with some information
channel_id = int(extra['channel_id']) channel_id = int(extra['channel_id'])

118
discord/automod.py

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

40
discord/channel.py

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

296
discord/client.py

@ -48,6 +48,7 @@ from typing import (
import aiohttp import aiohttp
from .sku import SKU, Entitlement
from .user import User, ClientUser from .user import User, ClientUser
from .invite import Invite from .invite import Invite
from .template import Template from .template import Template
@ -55,7 +56,7 @@ from .widget import Widget
from .guild import Guild from .guild import Guild
from .emoji import Emoji from .emoji import Emoji
from .channel import _threaded_channel_factory, PartialMessageable from .channel import _threaded_channel_factory, PartialMessageable
from .enums import ChannelType from .enums import ChannelType, EntitlementOwnerType
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .errors import * from .errors import *
from .enums import Status from .enums import Status
@ -72,6 +73,7 @@ from .backoff import ExponentialBackoff
from .webhook import Webhook from .webhook import Webhook
from .appinfo import AppInfo from .appinfo import AppInfo
from .ui.view import View from .ui.view import View
from .ui.dynamic import DynamicItem
from .stage_instance import StageInstance from .stage_instance import StageInstance
from .threads import Thread from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@ -82,7 +84,7 @@ if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu from .app_commands import Command, ContextMenu, MissingApplicationID
from .automod import AutoModAction, AutoModRule from .automod import AutoModAction, AutoModRule
from .channel import DMChannel, GroupChannel from .channel import DMChannel, GroupChannel
from .ext.commands import AutoShardedBot, Bot, Context, CommandError from .ext.commands import AutoShardedBot, Bot, Context, CommandError
@ -111,6 +113,7 @@ if TYPE_CHECKING:
from .scheduled_event import ScheduledEvent from .scheduled_event import ScheduledEvent
from .threads import ThreadMember from .threads import ThreadMember
from .types.guild import Guild as GuildPayload from .types.guild import Guild as GuildPayload
from .ui.item import Item
from .voice_client import VoiceProtocol from .voice_client import VoiceProtocol
from .audit_logs import AuditLogEntry from .audit_logs import AuditLogEntry
@ -672,7 +675,6 @@ class Client:
aiohttp.ClientError, aiohttp.ClientError,
asyncio.TimeoutError, asyncio.TimeoutError,
) as exc: ) as exc:
self.dispatch('disconnect') self.dispatch('disconnect')
if not reconnect: if not reconnect:
await self.close() await self.close()
@ -2241,8 +2243,8 @@ class Client:
Raises Raises
------ ------
Forbidden NotFound
You do not have access to the guild. The guild doesn't exist or you got no access to it.
HTTPException HTTPException
Getting the guild failed. Getting the guild failed.
@ -2630,6 +2632,242 @@ class Client:
# The type checker is not smart enough to figure out the constructor is correct # The type checker is not smart enough to figure out the constructor is correct
return cls(state=self._connection, data=data) # type: ignore 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]: async def fetch_premium_sticker_packs(self) -> List[StickerPack]:
"""|coro| """|coro|
@ -2678,6 +2916,54 @@ class Client:
data = await state.http.start_private_message(user.id) data = await state.http.start_private_message(user.id)
return state.add_dm_channel(data) 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: def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
"""Registers a :class:`~discord.ui.View` for persistent listening. """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. The string could not be converted into a colour.
""" """
if not value:
raise ValueError('unknown colour format given')
if value[0] == '#': if value[0] == '#':
return parse_hex_number(value[1:]) return parse_hex_number(value[1:])

86
discord/components.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload 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 .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag from .partial_emoji import PartialEmoji, _EmojiTag
@ -40,8 +40,10 @@ if TYPE_CHECKING:
ActionRow as ActionRowPayload, ActionRow as ActionRowPayload,
TextInput as TextInputPayload, TextInput as TextInputPayload,
ActionRowChildComponent as ActionRowChildComponentPayload, ActionRowChildComponent as ActionRowChildComponentPayload,
SelectDefaultValues as SelectDefaultValuesPayload,
) )
from .emoji import Emoji from .emoji import Emoji
from .abc import Snowflake
ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput'] ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput']
@ -53,6 +55,7 @@ __all__ = (
'SelectMenu', 'SelectMenu',
'SelectOption', 'SelectOption',
'TextInput', 'TextInput',
'SelectDefaultValue',
) )
@ -263,6 +266,7 @@ class SelectMenu(Component):
'options', 'options',
'disabled', 'disabled',
'channel_types', 'channel_types',
'default_values',
) )
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__ __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.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])]
self.disabled: bool = data.get('disabled', False) self.disabled: bool = data.get('disabled', False)
self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])] 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: def to_dict(self) -> SelectMenuPayload:
payload: 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, 'custom_id': self.custom_id,
'min_values': self.min_values, 'min_values': self.min_values,
'max_values': self.max_values, 'max_values': self.max_values,
@ -291,6 +298,8 @@ class SelectMenu(Component):
payload['options'] = [op.to_dict() for op in self.options] payload['options'] = [op.to_dict() for op in self.options]
if self.channel_types: if self.channel_types:
payload['channel_types'] = [t.value for t in 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 return payload
@ -512,6 +521,79 @@ class TextInput(Component):
return self.value 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 @overload
def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]: def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]:
... ...

275
discord/enums.py

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

14
discord/ext/commands/bot.py

@ -499,6 +499,12 @@ class BotBase(GroupMixin[None]):
``user`` parameter is now positional-only. ``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 Parameters
----------- -----------
user: :class:`.abc.User` user: :class:`.abc.User`
@ -516,9 +522,13 @@ class BotBase(GroupMixin[None]):
return user.id in self.owner_ids return user.id in self.owner_ids
else: else:
app = await self.application_info() # type: ignore app: discord.AppInfo = await self.application_info() # type: ignore
if app.team: 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 return user.id in ids
else: else:
self.owner_id = owner_id = app.owner.id 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 inspect
import discord import discord
import logging
from discord import app_commands from discord import app_commands
from discord.utils import maybe_coroutine, _to_kebab_case from discord.utils import maybe_coroutine, _to_kebab_case
@ -65,6 +66,7 @@ __all__ = (
FuncT = TypeVar('FuncT', bound=Callable[..., Any]) FuncT = TypeVar('FuncT', bound=Callable[..., Any])
MISSING: Any = discord.utils.MISSING MISSING: Any = discord.utils.MISSING
_log = logging.getLogger(__name__)
class CogMeta(type): class CogMeta(type):
@ -305,6 +307,7 @@ class Cog(metaclass=CogMeta):
# Register the application commands # Register the application commands
children: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = [] 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__: if cls.__cog_is_app_commands_group__:
group = app_commands.Group( group = app_commands.Group(
@ -331,6 +334,16 @@ class Cog(metaclass=CogMeta):
# Get the latest parent reference # Get the latest parent reference
parent = lookup[parent.qualified_name] # type: ignore 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 # Update our parent's reference to our self
parent.remove_command(command.name) # type: ignore parent.remove_command(command.name) # type: ignore
parent.add_command(command) # 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 # The type checker does not see the app_command attribute even though it exists
command.app_command = app_command # type: ignore 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__: if self.__cog_app_commands_group__:
children.append(app_command) # type: ignore # Somehow it thinks it can be None here 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: if len(mapping) > 25:
raise TypeError('maximum number of application command children exceeded') 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 return self
@ -753,7 +773,7 @@ class Cog(metaclass=CogMeta):
try: try:
await maybe_coroutine(self.cog_unload) await maybe_coroutine(self.cog_unload)
except Exception: except Exception:
pass _log.exception('Ignoring exception in cog unload for Cog %r (%r)', cls, self.qualified_name)
class GroupCog(Cog): class GroupCog(Cog):

2
discord/ext/commands/context.py

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

6
discord/ext/commands/core.py

@ -776,7 +776,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
command = self command = self
# command.parent is type-hinted as GroupMixin some attributes are resolved via MRO # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO
while command.parent is not None: # type: ignore while command.parent is not None: # type: ignore
command = command.parent # type: ignore command = command.parent
entries.append(command.name) # type: ignore entries.append(command.name) # type: ignore
return ' '.join(reversed(entries)) return ' '.join(reversed(entries))
@ -794,7 +794,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
entries = [] entries = []
command = self command = self
while command.parent is not None: # type: ignore while command.parent is not None: # type: ignore
command = command.parent # type: ignore command = command.parent
entries.append(command) entries.append(command)
return entries return entries
@ -2004,7 +2004,7 @@ def check_any(*checks: Check[ContextT]) -> Check[ContextT]:
# if we're here, all checks failed # if we're here, all checks failed
raise CheckAnyFailure(unwrapped, errors) raise CheckAnyFailure(unwrapped, errors)
return check(predicate) # type: ignore return check(predicate)
def has_role(item: Union[int, str], /) -> Check[Any]: 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 typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
from discord.errors import ClientException, DiscordException from discord.errors import ClientException, DiscordException
from discord.utils import _human_join
if TYPE_CHECKING: if TYPE_CHECKING:
from discord.abc import GuildChannel from discord.abc import GuildChannel
@ -758,12 +759,7 @@ class MissingAnyRole(CheckFailure):
self.missing_roles: SnowflakeList = missing_roles self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles] missing = [f"'{role}'" for role in missing_roles]
fmt = _human_join(missing)
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
message = f'You are missing at least one of the required roles: {fmt}' message = f'You are missing at least one of the required roles: {fmt}'
super().__init__(message) super().__init__(message)
@ -788,12 +784,7 @@ class BotMissingAnyRole(CheckFailure):
self.missing_roles: SnowflakeList = missing_roles self.missing_roles: SnowflakeList = missing_roles
missing = [f"'{role}'" for role in missing_roles] missing = [f"'{role}'" for role in missing_roles]
fmt = _human_join(missing)
if len(missing) > 2:
fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
message = f'Bot is missing at least one of the required roles: {fmt}' message = f'Bot is missing at least one of the required roles: {fmt}'
super().__init__(message) super().__init__(message)
@ -832,11 +823,7 @@ class MissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
fmt = _human_join(missing, final='and')
if len(missing) > 2:
fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = f'You are missing {fmt} permission(s) to run this command.' message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args) super().__init__(message, *args)
@ -857,11 +844,7 @@ class BotMissingPermissions(CheckFailure):
self.missing_permissions: List[str] = missing_permissions self.missing_permissions: List[str] = missing_permissions
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions]
fmt = _human_join(missing, final='and')
if len(missing) > 2:
fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = f'Bot requires {fmt} permission(s) to run this command.' message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args) super().__init__(message, *args)
@ -896,11 +879,7 @@ class BadUnionArgument(UserInputError):
return x.__class__.__name__ return x.__class__.__name__
to_string = [_get_name(x) for x in converters] to_string = [_get_name(x) for x in converters]
if len(to_string) > 2: fmt = _human_join(to_string)
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
else:
fmt = ' or '.join(to_string)
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into {fmt}.') 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 self.argument: str = argument
to_string = [repr(l) for l in literals] to_string = [repr(l) for l in literals]
if len(to_string) > 2: fmt = _human_join(to_string)
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
else:
fmt = ' or '.join(to_string)
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into the literal {fmt}.') 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(): for flag in flags.values():
if callable(flag.default): if callable(flag.default):
# Type checker does not understand that flag.default is a Callable # 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) setattr(self, flag.attribute, default)
else: else:
setattr(self, flag.attribute, flag.default) setattr(self, flag.attribute, flag.default)
@ -600,7 +600,7 @@ class FlagConverter(metaclass=FlagsMeta):
else: else:
if callable(flag.default): if callable(flag.default):
# Type checker does not understand flag.default is a Callable # 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) setattr(self, flag.attribute, default)
else: else:
setattr(self, flag.attribute, flag.default) 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]): 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) signature = inspect.signature(wrapped.callback)
params = replace_parameters(wrapped.params, wrapped.callback, signature) params = replace_parameters(wrapped.params, wrapped.callback, signature)
wrapped.callback.__signature__ = signature.replace(parameters=params) wrapped.callback.__signature__ = signature.replace(parameters=params)
nsfw = getattr(wrapped.callback, '__discord_app_commands_is_nsfw__', False) nsfw = getattr(wrapped.callback, '__discord_app_commands_is_nsfw__', False)
try: try:
super().__init__( 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 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 '', description=wrapped._locale_description or wrapped.description or wrapped.short_doc or '',
nsfw=nsfw, nsfw=nsfw,
@ -398,7 +404,7 @@ class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
if self.binding is not None: if self.binding is not None:
try: try:
# Type checker does not like runtime attribute retrieval # Type checker does not like runtime attribute retrieval
check: AppCommandCheck = self.binding.interaction_check # type: ignore check: AppCommandCheck = self.binding.interaction_check
except AttributeError: except AttributeError:
pass pass
else: else:
@ -594,6 +600,8 @@ class HybridGroup(Group[CogT, P, T]):
application command groups cannot be invoked, this creates a subcommand within application command groups cannot be invoked, this creates a subcommand within
the group that can be invoked with the given group callback. If ``None`` the group that can be invoked with the given group callback. If ``None``
then no fallback command is given. Defaults to ``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 __commands_is_hybrid__: ClassVar[bool] = True
@ -603,7 +611,7 @@ class HybridGroup(Group[CogT, P, T]):
*args: Any, *args: Any,
name: Union[str, app_commands.locale_str] = MISSING, name: Union[str, app_commands.locale_str] = MISSING,
description: 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, **attrs: Any,
) -> None: ) -> None:
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, 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 # 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 # make this type depend on the with_app_command bool without a lot of needless repetition
self.app_command: app_commands.Group = MISSING 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: Optional[str] = fallback
self.fallback_locale: Optional[app_commands.locale_str] = fallback_locale
if self.with_app_command: if self.with_app_command:
guild_ids = attrs.pop('guild_ids', None) or getattr( 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 self.app_command.module = self.module
if fallback is not None: if fallback is not None:
command = HybridAppCommand(self) command = HybridAppCommand(self, name=fallback_locale or fallback)
command.name = fallback
self.app_command.add_command(command) self.app_command.add_command(command)
@property @property
@ -920,9 +932,9 @@ def hybrid_group(
If the function is not a coroutine or is already a command. 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): if isinstance(func, Command):
raise TypeError('Callback is already a command.') raise TypeError('Callback is already a command.')
return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs) return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs)
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: if self._displayed_default is not empty:
return self._displayed_default 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 @property
def displayed_name(self) -> Optional[str]: def displayed_name(self) -> Optional[str]:
@ -197,7 +203,7 @@ class Parameter(inspect.Parameter):
""" """
# pre-condition: required is False # pre-condition: required is False
if callable(self.default): if callable(self.default):
return await maybe_coroutine(self.default, ctx) # type: ignore return await maybe_coroutine(self.default, ctx)
return self.default return self.default
@ -300,6 +306,7 @@ CurrentGuild = parameter(
displayed_default='<this server>', displayed_default='<this server>',
converter=GuildConverter, converter=GuildConverter,
) )
CurrentGuild._fallback = True
class Signature(inspect.Signature): class Signature(inspect.Signature):

241
discord/flags.py

@ -26,7 +26,21 @@ from __future__ import annotations
from functools import reduce from functools import reduce
from operator import or_ 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 from .enums import UserFlags
@ -44,6 +58,9 @@ __all__ = (
'ChannelFlags', 'ChannelFlags',
'AutoModPresets', 'AutoModPresets',
'MemberFlags', 'MemberFlags',
'AttachmentFlags',
'RoleFlags',
'SKUFlags',
) )
BF = TypeVar('BF', bound='BaseFlags') BF = TypeVar('BF', bound='BaseFlags')
@ -1195,7 +1212,7 @@ class Intents(BaseFlags):
""" """
return 1 << 16 return 1 << 16
@flag_value @alias_flag_value
def auto_moderation(self): def auto_moderation(self):
""":class:`bool`: Whether auto moderation related events are enabled. """:class:`bool`: Whether auto moderation related events are enabled.
@ -1618,10 +1635,19 @@ class ChannelFlags(BaseFlags):
""" """
return 1 << 4 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): class ArrayFlags(BaseFlags):
@classmethod @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) self = cls.__new__(cls)
# This is a micro-optimization given the frequency this object can be created. # This is a micro-optimization given the frequency this object can be created.
# (1).__lshift__ is used in place of lambda x: 1 << x # (1).__lshift__ is used in place of lambda x: 1 << x
@ -1810,3 +1836,212 @@ class MemberFlags(BaseFlags):
def started_onboarding(self): def started_onboarding(self):
""":class:`bool`: Returns ``True`` if the member has started onboarding.""" """:class:`bool`: Returns ``True`` if the member has started onboarding."""
return 1 << 3 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 traceback
import zlib 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 aiohttp
import yarl import yarl
@ -59,7 +59,7 @@ if TYPE_CHECKING:
from .client import Client from .client import Client
from .state import ConnectionState from .state import ConnectionState
from .voice_client import VoiceClient from .voice_state import VoiceConnectionState
class ReconnectWebSocket(Exception): class ReconnectWebSocket(Exception):
@ -132,11 +132,12 @@ class KeepAliveHandler(threading.Thread):
shard_id: Optional[int] = None, shard_id: Optional[int] = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> 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.ws: DiscordWebSocket = ws
self._main_thread_id: int = ws.thread_id self._main_thread_id: int = ws.thread_id
self.interval: Optional[float] = interval self.interval: Optional[float] = interval
self.daemon: bool = True
self.shard_id: Optional[int] = shard_id self.shard_id: Optional[int] = shard_id
self.msg: str = 'Keeping shard ID %s websocket alive with sequence %s.' self.msg: str = 'Keeping shard ID %s websocket alive with sequence %s.'
self.block_msg: str = 'Shard ID %s heartbeat blocked for more than %s seconds.' self.block_msg: str = 'Shard ID %s heartbeat blocked for more than %s seconds.'
@ -212,7 +213,8 @@ class KeepAliveHandler(threading.Thread):
class VoiceKeepAliveHandler(KeepAliveHandler): class VoiceKeepAliveHandler(KeepAliveHandler):
def __init__(self, *args: Any, **kwargs: Any) -> None: 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.recent_ack_latencies: Deque[float] = deque(maxlen=20)
self.msg: str = 'Keeping shard ID %s voice websocket alive with timestamp %s.' 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' 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: elif msg.type is aiohttp.WSMsgType.BINARY:
await self.received_message(msg.data) await self.received_message(msg.data)
elif msg.type is aiohttp.WSMsgType.ERROR: elif msg.type is aiohttp.WSMsgType.ERROR:
_log.debug('Received %s', msg) _log.debug('Received error %s', msg)
raise msg.data raise WebSocketClosure
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSE): elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSE):
_log.debug('Received %s', msg) _log.debug('Received %s', msg)
raise WebSocketClosure raise WebSocketClosure
@ -795,7 +797,7 @@ class DiscordVoiceWebSocket:
if TYPE_CHECKING: if TYPE_CHECKING:
thread_id: int thread_id: int
_connection: VoiceClient _connection: VoiceConnectionState
gateway: str gateway: str
_max_heartbeat_timeout: float _max_heartbeat_timeout: float
@ -864,16 +866,21 @@ class DiscordVoiceWebSocket:
await self.send_as_json(payload) await self.send_as_json(payload)
@classmethod @classmethod
async def from_client( async def from_connection_state(
cls, client: VoiceClient, *, resume: bool = False, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None cls,
state: VoiceConnectionState,
*,
resume: bool = False,
hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None,
) -> Self: ) -> Self:
"""Creates a voice websocket for the :class:`VoiceClient`.""" """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 http = client._state.http
socket = await http.ws_connect(gateway, compress=15) socket = await http.ws_connect(gateway, compress=15)
ws = cls(socket, loop=client.loop, hook=hook) ws = cls(socket, loop=client.loop, hook=hook)
ws.gateway = gateway ws.gateway = gateway
ws._connection = client ws._connection = state
ws._max_heartbeat_timeout = 60.0 ws._max_heartbeat_timeout = 60.0
ws.thread_id = threading.get_ident() ws.thread_id = threading.get_ident()
@ -949,29 +956,49 @@ class DiscordVoiceWebSocket:
state.voice_port = data['port'] state.voice_port = data['port']
state.endpoint_ip = data['ip'] 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) packet = bytearray(74)
struct.pack_into('>H', packet, 0, 1) # 1 = Send struct.pack_into('>H', packet, 0, 1) # 1 = Send
struct.pack_into('>H', packet, 2, 70) # 70 = Length struct.pack_into('>H', packet, 2, 70) # 70 = Length
struct.pack_into('>I', packet, 4, state.ssrc) 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('Sending ip discovery packet')
_log.debug('received packet in initial_connection: %s', recv) 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 # the ip is ascii starting at the 8th byte and ending at the first null
ip_start = 8 ip_start = 8
ip_end = recv.index(0, ip_start) 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] port = struct.unpack_from('>H', recv, len(recv) - 2)[0]
_log.debug('detected ip: %s port: %s', state.ip, state.port) _log.debug('detected ip: %s port: %s', ip, port)
# there *should* always be at least one supported mode (xsalsa20_poly1305) return ip, port
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)
@property @property
def latency(self) -> float: def latency(self) -> float:
@ -993,9 +1020,8 @@ class DiscordVoiceWebSocket:
self.secret_key = self._connection.secret_key = data['secret_key'] self.secret_key = self._connection.secret_key = data['secret_key']
# Send a speak command with the "not speaking" state. # Send a speak command with the "not speaking" state.
# This also tells Discord our SSRC value, which Discord requires # This also tells Discord our SSRC value, which Discord requires before
# before sending any voice data (and is the real reason why we # sending any voice data (and is the real reason why we call this here).
# call this here).
await self.speak(SpeakingState.none) await self.speak(SpeakingState.none)
async def poll_event(self) -> None: async def poll_event(self) -> None:
@ -1004,10 +1030,10 @@ class DiscordVoiceWebSocket:
if msg.type is aiohttp.WSMsgType.TEXT: if msg.type is aiohttp.WSMsgType.TEXT:
await self.received_message(utils._from_json(msg.data)) await self.received_message(utils._from_json(msg.data))
elif msg.type is aiohttp.WSMsgType.ERROR: 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 raise ConnectionClosed(self.ws, shard_id=None) from msg.data
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING): 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) raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code)
async def close(self, code: int = 1000) -> None: 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.integration import IntegrationType
from .types.snowflake import SnowflakeList from .types.snowflake import SnowflakeList
from .types.widget import EditWidgetSettings from .types.widget import EditWidgetSettings
from .types.audit_log import AuditLogEvent
from .message import EmojiInputType from .message import EmojiInputType
VocalGuildChannel = Union[VoiceChannel, StageChannel] VocalGuildChannel = Union[VoiceChannel, StageChannel]
@ -473,9 +474,15 @@ class Guild(Hashable):
role = Role(guild=self, data=r, state=state) role = Role(guild=self, data=r, state=state)
self._roles[role.id] = role self._roles[role.id] = role
self.emojis: Tuple[Emoji, ...] = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', []))) self.emojis: Tuple[Emoji, ...] = (
self.stickers: Tuple[GuildSticker, ...] = tuple( tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
map(lambda d: state.store_sticker(self, d), guild.get('stickers', [])) 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.features: List[GuildFeature] = guild.get('features', [])
self._splash: Optional[str] = guild.get('splash') self._splash: Optional[str] = guild.get('splash')
@ -2899,7 +2906,10 @@ class Guild(Hashable):
payload['tags'] = emoji payload['tags'] = emoji
data = await self._state.http.create_guild_sticker(self.id, payload, file, reason) 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: async def delete_sticker(self, sticker: Snowflake, /, *, reason: Optional[str] = None) -> None:
"""|coro| """|coro|
@ -3308,7 +3318,10 @@ class Guild(Hashable):
role_ids = [] role_ids = []
data = await self._state.http.create_custom_emoji(self.id, name, img, roles=role_ids, reason=reason) 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: async def delete_emoji(self, emoji: Snowflake, /, *, reason: Optional[str] = None) -> None:
"""|coro| """|coro|
@ -3596,7 +3609,7 @@ class Guild(Hashable):
The guild must have ``COMMUNITY`` in :attr:`~Guild.features`. 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 .. versionadded:: 2.0
@ -3855,7 +3868,7 @@ class Guild(Hashable):
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
before_id = before.id if before else None before_id = before.id if before else None
data = await self._state.http.get_audit_logs( 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', []) 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]): async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
after_id = after.id if after else None after_id = after.id if after else None
data = await self._state.http.get_audit_logs( 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', []) entries = data.get('audit_log_entries', [])
@ -3889,8 +3902,10 @@ class Guild(Hashable):
else: else:
user_id = None user_id = None
if action: if action is not MISSING:
action = action.value action_type: Optional[AuditLogEvent] = action.value
else:
action_type = None
if isinstance(before, datetime.datetime): if isinstance(before, datetime.datetime):
before = Object(id=utils.time_snowflake(before, high=False)) 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 .embeds import Embed
from .message import Attachment from .message import Attachment
from .flags import MessageFlags from .flags import MessageFlags
from .enums import AuditLogAction
from .types import ( from .types import (
appinfo, appinfo,
@ -92,6 +91,7 @@ if TYPE_CHECKING:
scheduled_event, scheduled_event,
sticker, sticker,
welcome_screen, welcome_screen,
sku,
) )
from .types.snowflake import Snowflake, SnowflakeList from .types.snowflake import Snowflake, SnowflakeList
@ -1728,7 +1728,7 @@ class HTTPClient:
before: Optional[Snowflake] = None, before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None, after: Optional[Snowflake] = None,
user_id: Optional[Snowflake] = None, user_id: Optional[Snowflake] = None,
action_type: Optional[AuditLogAction] = None, action_type: Optional[audit_log.AuditLogEvent] = None,
) -> Response[audit_log.AuditLog]: ) -> Response[audit_log.AuditLog]:
params: Dict[str, Any] = {'limit': limit} params: Dict[str, Any] = {'limit': limit}
if before: if before:
@ -1920,6 +1920,7 @@ class HTTPClient:
'topic', 'topic',
'privacy_level', 'privacy_level',
'send_start_notification', 'send_start_notification',
'guild_scheduled_event_id',
) )
payload = {k: v for k, v in payload.items() if k in valid_keys} payload = {k: v for k, v in payload.items() if k in valid_keys}
@ -2376,10 +2377,87 @@ class HTTPClient:
reason=reason, 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]: def get_guild_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]:
return self.request(Route('GET', '/guilds/{guild_id}/onboarding', guild_id=guild_id)) return self.request(Route('GET', '/guilds/{guild_id}/onboarding', guild_id=guild_id))
def modify_guild_onboarding( def edit_guild_onboarding(
self, self,
guild_id: Snowflake, guild_id: Snowflake,
*, *,

48
discord/interactions.py

@ -27,7 +27,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
import logging 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 asyncio
import datetime import datetime
@ -37,6 +37,7 @@ from .errors import InteractionResponded, HTTPException, ClientException, Discor
from .flags import MessageFlags from .flags import MessageFlags
from .channel import ChannelType from .channel import ChannelType
from ._types import ClientT from ._types import ClientT
from .sku import Entitlement
from .user import User from .user import User
from .member import Member from .member import Member
@ -110,6 +111,10 @@ class Interaction(Generic[ClientT]):
The channel the interaction was sent from. 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``. 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` application_id: :class:`int`
The application ID that the interaction was for. The application ID that the interaction was for.
user: Union[:class:`User`, :class:`Member`] user: Union[:class:`User`, :class:`Member`]
@ -150,6 +155,8 @@ class Interaction(Generic[ClientT]):
'guild_locale', 'guild_locale',
'extras', 'extras',
'command_failed', 'command_failed',
'entitlement_sku_ids',
'entitlements',
'_permissions', '_permissions',
'_app_permissions', '_app_permissions',
'_state', '_state',
@ -185,6 +192,8 @@ class Interaction(Generic[ClientT]):
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
self.channel: Optional[InteractionChannel] = None self.channel: Optional[InteractionChannel] = None
self.application_id: int = int(data['application_id']) 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.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
self.guild_locale: Optional[Locale] self.guild_locale: Optional[Locale]
@ -254,7 +263,10 @@ class Interaction(Generic[ClientT]):
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild the interaction was sent from.""" """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 @property
def channel_id(self) -> Optional[int]: def channel_id(self) -> Optional[int]:
@ -981,6 +993,38 @@ class InteractionResponse(Generic[ClientT]):
self._parent._state.store_view(modal) self._parent._state.store_view(modal)
self._response_type = InteractionResponseType.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: async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None:
"""|coro| """|coro|

3
discord/invite.py

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

4
discord/member.py

@ -840,7 +840,7 @@ class Member(discord.abc.Messageable, _UserTag):
Raises Raises
------- -------
Forbidden 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 HTTPException
The operation failed. The operation failed.
TypeError TypeError
@ -932,7 +932,7 @@ class Member(discord.abc.Messageable, _UserTag):
ClientException ClientException
You are not connected to a voice channel. You are not connected to a voice channel.
Forbidden 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 HTTPException
The operation failed. The operation failed.
""" """

10
discord/message.py

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

2
discord/oggparse.py

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

64
discord/opus.py

@ -39,10 +39,17 @@ from .errors import DiscordException
if TYPE_CHECKING: if TYPE_CHECKING:
T = TypeVar('T') T = TypeVar('T')
APPLICATION_CTL = Literal['audio', 'voip', 'lowdelay']
BAND_CTL = Literal['narrow', 'medium', 'wide', 'superwide', 'full'] BAND_CTL = Literal['narrow', 'medium', 'wide', 'superwide', 'full']
SIGNAL_CTL = Literal['auto', 'voice', 'music'] SIGNAL_CTL = Literal['auto', 'voice', 'music']
class ApplicationCtl(TypedDict):
audio: int
voip: int
lowdelay: int
class BandCtl(TypedDict): class BandCtl(TypedDict):
narrow: int narrow: int
medium: int medium: int
@ -65,6 +72,8 @@ __all__ = (
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
OPUS_SILENCE = b'\xF8\xFF\xFE'
c_int_ptr = ctypes.POINTER(ctypes.c_int) c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16) c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
c_float_ptr = ctypes.POINTER(ctypes.c_float) c_float_ptr = ctypes.POINTER(ctypes.c_float)
@ -90,9 +99,10 @@ OK = 0
BAD_ARG = -1 BAD_ARG = -1
# Encoder CTLs # Encoder CTLs
APPLICATION_AUDIO = 2049 APPLICATION_AUDIO = 'audio'
APPLICATION_VOIP = 2048 APPLICATION_VOIP = 'voip'
APPLICATION_LOWDELAY = 2051 APPLICATION_LOWDELAY = 'lowdelay'
# These remain as strings for backwards compat
CTL_SET_BITRATE = 4002 CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008 CTL_SET_BANDWIDTH = 4008
@ -105,6 +115,12 @@ CTL_SET_GAIN = 4034
CTL_LAST_PACKET_DURATION = 4039 CTL_LAST_PACKET_DURATION = 4039
# fmt: on # fmt: on
application_ctl: ApplicationCtl = {
'audio': 2049,
'voip': 2048,
'lowdelay': 2051,
}
band_ctl: BandCtl = { band_ctl: BandCtl = {
'narrow': 1101, 'narrow': 1101,
'medium': 1102, 'medium': 1102,
@ -319,16 +335,38 @@ class _OpusStruct:
class Encoder(_OpusStruct): class Encoder(_OpusStruct):
def __init__(self, application: int = APPLICATION_AUDIO): def __init__(
_OpusStruct.get_opus_version() self,
*,
self.application: int = application 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._state: EncoderStruct = self._create_state()
self.set_bitrate(128)
self.set_fec(True) self.set_bitrate(bitrate)
self.set_expected_packet_loss_percent(0.15) self.set_fec(fec)
self.set_bandwidth('full') if fec:
self.set_signal_type('auto') self.set_expected_packet_loss_percent(expected_packet_loss)
self.set_bandwidth(bandwidth)
self.set_signal_type(signal_type)
def __del__(self) -> None: def __del__(self) -> None:
if hasattr(self, '_state'): if hasattr(self, '_state'):
@ -355,7 +393,7 @@ class Encoder(_OpusStruct):
def set_signal_type(self, req: SIGNAL_CTL) -> None: def set_signal_type(self, req: SIGNAL_CTL) -> None:
if req not in signal_ctl: 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] k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k) _lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)

49
discord/permissions.py

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

133
discord/player.py

@ -25,6 +25,7 @@ from __future__ import annotations
import threading import threading
import subprocess import subprocess
import warnings
import audioop import audioop
import asyncio import asyncio
import logging import logging
@ -39,7 +40,7 @@ from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, T
from .enums import SpeakingState from .enums import SpeakingState
from .errors import ClientException from .errors import ClientException
from .opus import Encoder as OpusEncoder from .opus import Encoder as OpusEncoder, OPUS_SILENCE
from .oggparse import OggStream from .oggparse import OggStream
from .utils import MISSING from .utils import MISSING
@ -145,6 +146,8 @@ class FFmpegAudio(AudioSource):
.. versionadded:: 1.3 .. versionadded:: 1.3
""" """
BLOCKSIZE: int = io.DEFAULT_BUFFER_SIZE
def __init__( def __init__(
self, self,
source: Union[str, io.BufferedIOBase], source: Union[str, io.BufferedIOBase],
@ -153,12 +156,25 @@ class FFmpegAudio(AudioSource):
args: Any, args: Any,
**subprocess_kwargs: Any, **subprocess_kwargs: Any,
): ):
piping = subprocess_kwargs.get('stdin') == subprocess.PIPE piping_stdin = subprocess_kwargs.get('stdin') == subprocess.PIPE
if piping and isinstance(source, str): if piping_stdin and isinstance(source, str):
raise TypeError("parameter conflict: 'source' parameter cannot be a string when piping to stdin") 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] args = [executable, *args]
kwargs = {'stdout': subprocess.PIPE} kwargs = {'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE if piping_stderr else stderr}
kwargs.update(subprocess_kwargs) kwargs.update(subprocess_kwargs)
# Ensure attribute is assigned even in the case of errors # 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._process = self._spawn_process(args, **kwargs)
self._stdout: IO[bytes] = self._process.stdout # type: ignore # process stdout is explicitly set self._stdout: IO[bytes] = self._process.stdout # type: ignore # process stdout is explicitly set
self._stdin: Optional[IO[bytes]] = None 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: if piping_stdin:
n = f'popen-stdin-writer:{id(self):#x}' n = f'popen-stdin-writer:pid-{self._process.pid}'
self._stdin = self._process.stdin self._stdin = self._process.stdin
self._pipe_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n) self._pipe_writer_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n)
self._pipe_thread.start() 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: def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen:
_log.debug('Spawning ffmpeg process with command: %s', args)
process = None process = None
try: try:
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs) process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)
@ -187,7 +212,8 @@ class FFmpegAudio(AudioSource):
return process return process
def _kill_process(self) -> None: 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: if proc is MISSING:
return return
@ -207,8 +233,7 @@ class FFmpegAudio(AudioSource):
def _pipe_writer(self, source: io.BufferedIOBase) -> None: def _pipe_writer(self, source: io.BufferedIOBase) -> None:
while self._process: while self._process:
# arbitrarily large read size data = source.read(self.BLOCKSIZE)
data = source.read(8192)
if not data: if not data:
if self._stdin is not None: if self._stdin is not None:
self._stdin.close() self._stdin.close()
@ -222,9 +247,27 @@ class FFmpegAudio(AudioSource):
self._process.terminate() self._process.terminate()
return 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: def cleanup(self) -> None:
self._kill_process() self._kill_process()
self._process = self._stdout = self._stdin = MISSING self._process = self._stdout = self._stdin = self._stderr = MISSING
class FFmpegPCMAudio(FFmpegAudio): class FFmpegPCMAudio(FFmpegAudio):
@ -250,7 +293,6 @@ class FFmpegPCMAudio(FFmpegAudio):
to the stdin of ffmpeg. Defaults to ``False``. to the stdin of ffmpeg. Defaults to ``False``.
stderr: Optional[:term:`py:file object`] stderr: Optional[:term:`py:file object`]
A file-like object to pass to the Popen constructor. A file-like object to pass to the Popen constructor.
Could also be an instance of ``subprocess.PIPE``.
before_options: Optional[:class:`str`] before_options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg before the ``-i`` flag. Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`] options: Optional[:class:`str`]
@ -268,7 +310,7 @@ class FFmpegPCMAudio(FFmpegAudio):
*, *,
executable: str = 'ffmpeg', executable: str = 'ffmpeg',
pipe: bool = False, pipe: bool = False,
stderr: Optional[IO[str]] = None, stderr: Optional[IO[bytes]] = None,
before_options: Optional[str] = None, before_options: Optional[str] = None,
options: Optional[str] = None, options: Optional[str] = None,
) -> None: ) -> None:
@ -280,7 +322,14 @@ class FFmpegPCMAudio(FFmpegAudio):
args.append('-i') args.append('-i')
args.append('-' if pipe else source) 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): if isinstance(options, str):
args.extend(shlex.split(options)) args.extend(shlex.split(options))
@ -348,7 +397,6 @@ class FFmpegOpusAudio(FFmpegAudio):
to the stdin of ffmpeg. Defaults to ``False``. to the stdin of ffmpeg. Defaults to ``False``.
stderr: Optional[:term:`py:file object`] stderr: Optional[:term:`py:file object`]
A file-like object to pass to the Popen constructor. A file-like object to pass to the Popen constructor.
Could also be an instance of ``subprocess.PIPE``.
before_options: Optional[:class:`str`] before_options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg before the ``-i`` flag. Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`] options: Optional[:class:`str`]
@ -381,7 +429,7 @@ class FFmpegOpusAudio(FFmpegAudio):
args.append('-i') args.append('-i')
args.append('-' if pipe else source) 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 bitrate = bitrate if bitrate is not None else 128
# fmt: off # fmt: off
@ -391,7 +439,10 @@ class FFmpegOpusAudio(FFmpegAudio):
'-ar', '48000', '-ar', '48000',
'-ac', '2', '-ac', '2',
'-b:a', f'{bitrate}k', '-b:a', f'{bitrate}k',
'-loglevel', 'warning')) '-loglevel', 'warning',
'-fec', 'true',
'-packet_loss', '15',
'-blocksize', str(self.BLOCKSIZE)))
# fmt: on # fmt: on
if isinstance(options, str): if isinstance(options, str):
@ -643,8 +694,7 @@ class AudioPlayer(threading.Thread):
*, *,
after: Optional[Callable[[Optional[Exception]], Any]] = None, after: Optional[Callable[[Optional[Exception]], Any]] = None,
) -> None: ) -> None:
threading.Thread.__init__(self) super().__init__(daemon=True, name=f'audio-player:{id(self):#x}')
self.daemon: bool = True
self.source: AudioSource = source self.source: AudioSource = source
self.client: VoiceClient = client self.client: VoiceClient = client
self.after: Optional[Callable[[Optional[Exception]], Any]] = after self.after: Optional[Callable[[Optional[Exception]], Any]] = after
@ -653,7 +703,6 @@ class AudioPlayer(threading.Thread):
self._resumed: threading.Event = threading.Event() self._resumed: threading.Event = threading.Event()
self._resumed.set() # we are not paused self._resumed.set() # we are not paused
self._current_error: Optional[Exception] = None self._current_error: Optional[Exception] = None
self._connected: threading.Event = client._connected
self._lock: threading.Lock = threading.Lock() self._lock: threading.Lock = threading.Lock()
if after is not None and not callable(after): if after is not None and not callable(after):
@ -664,36 +713,46 @@ class AudioPlayer(threading.Thread):
self._start = time.perf_counter() self._start = time.perf_counter()
# getattr lookup speed ups # getattr lookup speed ups
play_audio = self.client.send_audio_packet client = self.client
play_audio = client.send_audio_packet
self._speak(SpeakingState.voice) self._speak(SpeakingState.voice)
while not self._end.is_set(): while not self._end.is_set():
# are we paused? # are we paused?
if not self._resumed.is_set(): if not self._resumed.is_set():
self.send_silence()
# wait until we aren't # wait until we aren't
self._resumed.wait() self._resumed.wait()
continue 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() data = self.source.read()
if not data: if not data:
self.stop() self.stop()
break 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()) play_audio(data, encode=not self.source.is_opus())
self.loops += 1
next_time = self._start + self.DELAY * self.loops next_time = self._start + self.DELAY * self.loops
delay = max(0, self.DELAY + (next_time - time.perf_counter())) delay = max(0, self.DELAY + (next_time - time.perf_counter()))
time.sleep(delay) time.sleep(delay)
self.send_silence()
def run(self) -> None: def run(self) -> None:
try: try:
self._do_run() self._do_run()
@ -739,7 +798,7 @@ class AudioPlayer(threading.Thread):
def is_paused(self) -> bool: def is_paused(self) -> bool:
return not self._end.is_set() and not self._resumed.is_set() 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: with self._lock:
self.pause(update_speaking=False) self.pause(update_speaking=False)
self.source = source self.source = source
@ -750,3 +809,11 @@ class AudioPlayer(threading.Thread):
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.client.loop) asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.client.loop)
except Exception: except Exception:
_log.exception("Speaking call in player failed") _log.exception("Speaking call in player failed")
def send_silence(self, count: int = 5) -> None:
try:
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 from __future__ import annotations
import datetime 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 .enums import ChannelType, try_enum
from .utils import _get_as_snowflake from .utils import _get_as_snowflake
from .app_commands import AppCommandPermissions from .app_commands import AppCommandPermissions
from .colour import Colour
if TYPE_CHECKING: if TYPE_CHECKING:
from .types.gateway import ( from .types.gateway import (
@ -57,6 +58,7 @@ if TYPE_CHECKING:
from .guild import Guild from .guild import Guild
ReactionActionEvent = Union[MessageReactionAddEvent, MessageReactionRemoveEvent] ReactionActionEvent = Union[MessageReactionAddEvent, MessageReactionRemoveEvent]
ReactionActionType = Literal['REACTION_ADD', 'REACTION_REMOVE']
__all__ = ( __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. The member who added the reaction. Only available if ``event_type`` is ``REACTION_ADD`` and the reaction is inside a guild.
.. versionadded:: 1.3 .. 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` event_type: :class:`str`
The event type that triggered this action. Can be The event type that triggered this action. Can be
``REACTION_ADD`` for reaction addition or ``REACTION_ADD`` for reaction addition or
``REACTION_REMOVE`` for reaction removal. ``REACTION_REMOVE`` for reaction removal.
.. versionadded:: 1.3 .. 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.message_id: int = int(data['message_id'])
self.channel_id: int = int(data['channel_id']) self.channel_id: int = int(data['channel_id'])
self.user_id: int = int(data['user_id']) self.user_id: int = int(data['user_id'])
self.emoji: PartialEmoji = emoji self.emoji: PartialEmoji = emoji
self.event_type: str = event_type self.event_type: ReactionActionType = event_type
self.member: Optional[Member] = None 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: try:
self.guild_id: Optional[int] = int(data['guild_id']) self.guild_id: Optional[int] = int(data['guild_id'])
except KeyError: except KeyError:
self.guild_id: Optional[int] = None 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): class RawReactionClearEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_reaction_clear` event. """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`] emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]
The reaction emoji. May be a custom emoji, or a unicode emoji. The reaction emoji. May be a custom emoji, or a unicode emoji.
count: :class:`int` 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` me: :class:`bool`
If the user sent this reaction. If the user sent this reaction.
message: :class:`Message` message: :class:`Message`
Message this reaction is for. 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): def __init__(self, *, message: Message, data: ReactionPayload, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None):
self.message: Message = message self.message: Message = message
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_emoji_from_partial_payload(data['emoji']) self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_emoji_from_partial_payload(data['emoji'])
self.count: int = data.get('count', 1) self.count: int = data.get('count', 1)
self.me: bool = data['me'] 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: def is_custom_emoji(self) -> bool:
""":class:`bool`: If this is a custom emoji.""" """: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 .colour import Colour
from .mixins import Hashable from .mixins import Hashable
from .utils import snowflake_time, _bytes_to_base64_data, _get_as_snowflake, MISSING from .utils import snowflake_time, _bytes_to_base64_data, _get_as_snowflake, MISSING
from .flags import RoleFlags
__all__ = ( __all__ = (
'RoleTags', 'RoleTags',
@ -219,6 +220,7 @@ class Role(Hashable):
'hoist', 'hoist',
'guild', 'guild',
'tags', 'tags',
'_flags',
'_state', '_state',
) )
@ -281,6 +283,7 @@ class Role(Hashable):
self.managed: bool = data.get('managed', False) self.managed: bool = data.get('managed', False)
self.mentionable: bool = data.get('mentionable', False) self.mentionable: bool = data.get('mentionable', False)
self.tags: Optional[RoleTags] self.tags: Optional[RoleTags]
self._flags: int = data.get('flags', 0)
try: try:
self.tags = RoleTags(data['tags']) self.tags = RoleTags(data['tags'])
@ -379,6 +382,14 @@ class Role(Hashable):
role_id = self.id role_id = self.id
return [member for member in all_members if member._roles.has(role_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: async def _move(self, position: int, reason: Optional[str]) -> None:
if position <= 0: if position <= 0:
raise ValueError("Cannot move role to position 0 or below") 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) self.ws = await asyncio.wait_for(coro, timeout=60.0)
except self._handled_exceptions as e: except self._handled_exceptions as e:
await self._handle_disconnect(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: except asyncio.CancelledError:
return return
except Exception as e: 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, Dict,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
Type,
Union, Union,
Callable, Callable,
Any, Any,
@ -52,6 +53,7 @@ import os
from .guild import Guild from .guild import Guild
from .activity import BaseActivity from .activity import BaseActivity
from .sku import Entitlement
from .user import User, ClientUser from .user import User, ClientUser
from .emoji import Emoji from .emoji import Emoji
from .mentions import AllowedMentions from .mentions import AllowedMentions
@ -84,6 +86,8 @@ if TYPE_CHECKING:
from .http import HTTPClient from .http import HTTPClient
from .voice_client import VoiceProtocol from .voice_client import VoiceProtocol
from .gateway import DiscordWebSocket from .gateway import DiscordWebSocket
from .ui.item import Item
from .ui.dynamic import DynamicItem
from .app_commands import CommandTree, Translator from .app_commands import CommandTree, Translator
from .types.automod import AutoModerationRule, AutoModerationActionExecution from .types.automod import AutoModerationRule, AutoModerationActionExecution
@ -259,6 +263,13 @@ class ConnectionState(Generic[ClientT]):
self.clear() 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: async def close(self) -> None:
for voice in self.voice_clients: for voice in self.voice_clients:
try: try:
@ -388,6 +399,12 @@ class ConnectionState(Generic[ClientT]):
def prevent_view_updates_for(self, message_id: int) -> Optional[View]: def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
return self._view_store.remove_message_tracking(message_id) 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 @property
def persistent_views(self) -> Sequence[View]: def persistent_views(self) -> Sequence[View]:
return self._view_store.persistent_views return self._view_store.persistent_views
@ -1568,6 +1585,18 @@ class ConnectionState(Generic[ClientT]):
self.dispatch('raw_typing', raw) 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]]: def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
if isinstance(channel, (TextChannel, Thread, VoiceChannel)): if isinstance(channel, (TextChannel, Thread, VoiceChannel)):
return channel.guild.get_member(user_id) return channel.guild.get_member(user_id)

11
discord/team.py

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

21
discord/template.py

@ -69,6 +69,10 @@ class _PartialTemplateState:
def member_cache_flags(self): def member_cache_flags(self):
return self.__state.member_cache_flags return self.__state.member_cache_flags
@property
def cache_guild_expressions(self):
return False
def store_emoji(self, guild, packet) -> None: def store_emoji(self, guild, packet) -> None:
return None return None
@ -146,18 +150,11 @@ class Template:
self.created_at: Optional[datetime.datetime] = parse_time(data.get('created_at')) self.created_at: Optional[datetime.datetime] = parse_time(data.get('created_at'))
self.updated_at: Optional[datetime.datetime] = parse_time(data.get('updated_at')) self.updated_at: Optional[datetime.datetime] = parse_time(data.get('updated_at'))
guild_id = int(data['source_guild_id']) source_serialised = data['serialized_source_guild']
guild: Optional[Guild] = self._state._get_guild(guild_id) source_serialised['id'] = int(data['source_guild_id'])
state = _PartialTemplateState(state=self._state)
self.source_guild: Guild # Guild expects a ConnectionState, we're passing a _PartialTemplateState
if guild is None: self.source_guild = Guild(data=source_serialised, state=state) # type: ignore
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
self.is_dirty: Optional[bool] = data.get('is_dirty', None) 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 .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMode, PermissionOverwrite, ForumTag
from .threads import Thread from .threads import Thread
from .command import ApplicationCommand, ApplicationCommandPermissions from .command import ApplicationCommand, ApplicationCommandPermissions
from .automod import AutoModerationTriggerMetadata
from .onboarding import PromptOption, Prompt from .onboarding import PromptOption, Prompt
AuditLogEvent = Literal[ AuditLogEvent = Literal[
@ -94,6 +95,8 @@ AuditLogEvent = Literal[
143, 143,
144, 144,
145, 145,
150,
151,
163, 163,
164, 164,
167, 167,
@ -285,6 +288,12 @@ class _AuditLogChange_DefaultReactionEmoji(TypedDict):
old_value: Optional[DefaultReaction] old_value: Optional[DefaultReaction]
class _AuditLogChange_TriggerMetadata(TypedDict):
key: Literal['trigger_metadata']
new_value: Optional[AutoModerationTriggerMetadata]
old_value: Optional[AutoModerationTriggerMetadata]
class _AuditLogChange_Prompts(TypedDict): class _AuditLogChange_Prompts(TypedDict):
key: Literal['prompts'] key: Literal['prompts']
new_value: List[Prompt] new_value: List[Prompt]
@ -319,6 +328,7 @@ AuditLogChange = Union[
_AuditLogChange_SnowflakeList, _AuditLogChange_SnowflakeList,
_AuditLogChange_AvailableTags, _AuditLogChange_AvailableTags,
_AuditLogChange_DefaultReactionEmoji, _AuditLogChange_DefaultReactionEmoji,
_AuditLogChange_TriggerMetadata,
_AuditLogChange_Prompts, _AuditLogChange_Prompts,
_AuditLogChange_Options, _AuditLogChange_Options,
] ]
@ -337,6 +347,7 @@ class AuditEntryInfo(TypedDict):
guild_id: Snowflake guild_id: Snowflake
auto_moderation_rule_name: str auto_moderation_rule_name: str
auto_moderation_rule_trigger_type: str auto_moderation_rule_trigger_type: str
integration_type: str
class AuditLogEntry(TypedDict): class AuditLogEntry(TypedDict):

1
discord/types/automod.py

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

17
discord/types/channel.py

@ -40,7 +40,7 @@ class PermissionOverwrite(TypedDict):
deny: str 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] ChannelType = Union[ChannelTypeWithoutThread, ThreadType]
@ -138,8 +138,7 @@ ForumOrderType = Literal[0, 1]
ForumLayoutType = Literal[0, 1, 2] ForumLayoutType = Literal[0, 1, 2]
class ForumChannel(_BaseTextChannel): class _BaseForumChannel(_BaseTextChannel):
type: Literal[15]
available_tags: List[ForumTag] available_tags: List[ForumTag]
default_reaction_emoji: Optional[DefaultReaction] default_reaction_emoji: Optional[DefaultReaction]
default_sort_order: Optional[ForumOrderType] default_sort_order: Optional[ForumOrderType]
@ -147,7 +146,17 @@ class ForumChannel(_BaseTextChannel):
flags: NotRequired[int] 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): class _BaseDMChannel(_BaseChannel):

11
discord/types/components.py

@ -33,6 +33,7 @@ from .channel import ChannelType
ComponentType = Literal[1, 2, 3, 4] ComponentType = Literal[1, 2, 3, 4]
ButtonStyle = Literal[1, 2, 3, 4, 5] ButtonStyle = Literal[1, 2, 3, 4, 5]
TextStyle = Literal[1, 2] TextStyle = Literal[1, 2]
DefaultValueType = Literal['user', 'role', 'channel']
class ActionRow(TypedDict): class ActionRow(TypedDict):
@ -66,6 +67,11 @@ class SelectComponent(TypedDict):
disabled: NotRequired[bool] disabled: NotRequired[bool]
class SelectDefaultValues(TypedDict):
id: int
type: DefaultValueType
class StringSelectComponent(SelectComponent): class StringSelectComponent(SelectComponent):
type: Literal[3] type: Literal[3]
options: NotRequired[List[SelectOption]] options: NotRequired[List[SelectOption]]
@ -73,19 +79,23 @@ class StringSelectComponent(SelectComponent):
class UserSelectComponent(SelectComponent): class UserSelectComponent(SelectComponent):
type: Literal[5] type: Literal[5]
default_values: NotRequired[List[SelectDefaultValues]]
class RoleSelectComponent(SelectComponent): class RoleSelectComponent(SelectComponent):
type: Literal[6] type: Literal[6]
default_values: NotRequired[List[SelectDefaultValues]]
class MentionableSelectComponent(SelectComponent): class MentionableSelectComponent(SelectComponent):
type: Literal[7] type: Literal[7]
default_values: NotRequired[List[SelectDefaultValues]]
class ChannelSelectComponent(SelectComponent): class ChannelSelectComponent(SelectComponent):
type: Literal[8] type: Literal[8]
channel_types: NotRequired[List[ChannelType]] channel_types: NotRequired[List[ChannelType]]
default_values: NotRequired[List[SelectDefaultValues]]
class TextInput(TypedDict): class TextInput(TypedDict):
@ -104,6 +114,7 @@ class SelectMenu(SelectComponent):
type: Literal[3, 5, 6, 7, 8] type: Literal[3, 5, 6, 7, 8]
options: NotRequired[List[SelectOption]] options: NotRequired[List[SelectOption]]
channel_types: NotRequired[List[ChannelType]] channel_types: NotRequired[List[ChannelType]]
default_values: NotRequired[List[SelectDefaultValues]]
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] 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 .automod import AutoModerationAction, AutoModerationRuleTriggerType
from .activity import PartialPresenceUpdate from .activity import PartialPresenceUpdate
from .sku import Entitlement
from .voice import GuildVoiceState from .voice import GuildVoiceState
from .integration import BaseIntegration, IntegrationApplication from .integration import BaseIntegration, IntegrationApplication
from .role import Role from .role import Role
@ -100,6 +101,9 @@ class MessageReactionAddEvent(TypedDict):
emoji: PartialEmoji emoji: PartialEmoji
member: NotRequired[MemberWithUser] member: NotRequired[MemberWithUser]
guild_id: NotRequired[Snowflake] guild_id: NotRequired[Snowflake]
message_author_id: NotRequired[Snowflake]
burst: bool
burst_colors: NotRequired[List[str]]
class MessageReactionRemoveEvent(TypedDict): class MessageReactionRemoveEvent(TypedDict):
@ -108,6 +112,7 @@ class MessageReactionRemoveEvent(TypedDict):
message_id: Snowflake message_id: Snowflake
emoji: PartialEmoji emoji: PartialEmoji
guild_id: NotRequired[Snowflake] guild_id: NotRequired[Snowflake]
burst: bool
class MessageReactionRemoveAllEvent(TypedDict): class MessageReactionRemoveAllEvent(TypedDict):
@ -343,3 +348,6 @@ class AutoModerationActionExecution(TypedDict):
class GuildAuditLogEntryCreate(AuditLogEntry): class GuildAuditLogEntryCreate(AuditLogEntry):
guild_id: Snowflake 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 typing_extensions import NotRequired
from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel
from .sku import Entitlement
from .threads import ThreadType from .threads import ThreadType
from .member import Member from .member import Member
from .message import Attachment from .message import Attachment
@ -208,6 +209,8 @@ class _BaseInteraction(TypedDict):
app_permissions: NotRequired[str] app_permissions: NotRequired[str]
locale: NotRequired[str] locale: NotRequired[str]
guild_locale: NotRequired[str] guild_locale: NotRequired[str]
entitlement_sku_ids: NotRequired[List[Snowflake]]
entitlements: NotRequired[List[Entitlement]]
class PingInteraction(_BaseInteraction): class PingInteraction(_BaseInteraction):

9
discord/types/message.py

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

1
discord/types/role.py

@ -39,6 +39,7 @@ class Role(TypedDict):
permissions: str permissions: str
managed: bool managed: bool
mentionable: bool mentionable: bool
flags: int
icon: NotRequired[Optional[str]] icon: NotRequired[Optional[str]]
unicode_emoji: NotRequired[Optional[str]] unicode_emoji: NotRequired[Optional[str]]
tags: NotRequired[RoleTags] 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 .snowflake import Snowflake
from .user import User from .user import User
StickerFormatType = Literal[1, 2, 3] StickerFormatType = Literal[1, 2, 3, 4]
class StickerItem(TypedDict): class StickerItem(TypedDict):

3
discord/types/team.py

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

2
discord/types/user.py

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

1
discord/ui/__init__.py

@ -15,3 +15,4 @@ from .item import *
from .button import * from .button import *
from .select import * from .select import *
from .text_input 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. The interaction that triggered this UI item.
""" """
pass 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. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations 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 from contextvars import ContextVar
import inspect import inspect
import os import os
from .item import Item, ItemCallbackType from .item import Item, ItemCallbackType
from ..enums import ChannelType, ComponentType from ..enums import ChannelType, ComponentType, SelectDefaultValueType
from ..partial_emoji import PartialEmoji from ..partial_emoji import PartialEmoji
from ..emoji import Emoji from ..emoji import Emoji
from ..utils import MISSING from ..utils import MISSING, _human_join
from ..components import ( from ..components import (
SelectOption, SelectOption,
SelectMenu, SelectMenu,
SelectDefaultValue,
) )
from ..app_commands.namespace import Namespace 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__ = ( __all__ = (
'Select', 'Select',
@ -48,15 +69,12 @@ __all__ = (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import TypeAlias, Self from typing_extensions import TypeAlias, Self, TypeGuard
from .view import View from .view import View
from ..types.components import SelectMenu as SelectMenuPayload from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import SelectMessageComponentInteractionData from ..types.interactions import SelectMessageComponentInteractionData
from ..app_commands import AppCommandChannel, AppCommandThread from ..app_commands import AppCommandChannel, AppCommandThread
from ..member import Member
from ..role import Role
from ..user import User
from ..interactions import Interaction from ..interactions import Interaction
ValidSelectType: TypeAlias = Literal[ ValidSelectType: TypeAlias = Literal[
@ -69,6 +87,18 @@ if TYPE_CHECKING:
PossibleValue: TypeAlias = Union[ PossibleValue: TypeAlias = Union[
str, User, Member, Role, AppCommandChannel, AppCommandThread, Union[Role, Member], Union[Role, User] 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) V = TypeVar('V', bound='View', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]') BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
@ -78,10 +108,81 @@ RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]')
ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]') ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]')
MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]') MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]')
SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] 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') 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]): class BaseSelect(Item[V]):
"""The base Select model that all other Select models inherit from. """The base Select model that all other Select models inherit from.
@ -115,6 +216,13 @@ class BaseSelect(Item[V]):
'max_values', 'max_values',
'disabled', 'disabled',
) )
__component_attributes__: Tuple[str, ...] = (
'custom_id',
'placeholder',
'min_values',
'max_values',
'disabled',
)
def __init__( def __init__(
self, self,
@ -128,6 +236,7 @@ class BaseSelect(Item[V]):
disabled: bool = False, disabled: bool = False,
options: List[SelectOption] = MISSING, options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING, channel_types: List[ChannelType] = MISSING,
default_values: Sequence[SelectDefaultValue] = MISSING,
) -> None: ) -> None:
super().__init__() super().__init__()
self._provided_custom_id = custom_id is not MISSING self._provided_custom_id = custom_id is not MISSING
@ -144,6 +253,7 @@ class BaseSelect(Item[V]):
disabled=disabled, disabled=disabled,
channel_types=[] if channel_types is MISSING else channel_types, channel_types=[] if channel_types is MISSING else channel_types,
options=[] if options is MISSING else options, options=[] if options is MISSING else options,
default_values=[] if default_values is MISSING else default_values,
) )
self.row = row self.row = row
@ -233,10 +343,16 @@ class BaseSelect(Item[V]):
@classmethod @classmethod
def from_component(cls, component: SelectMenu) -> Self: def from_component(cls, component: SelectMenu) -> Self:
return cls( type_to_cls: Dict[ComponentType, Type[BaseSelect[Any]]] = {
**{k: getattr(component, k) for k in cls.__item_repr_attributes__}, ComponentType.string_select: Select,
row=None, 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]): 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). 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__( def __init__(
self, self,
@ -409,6 +525,10 @@ class UserSelect(BaseSelect[V]):
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. 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`] row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5 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 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). ordering. The row number must be between 0 and 4 (i.e. zero indexed).
""" """
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__( def __init__(
self, self,
*, *,
@ -426,6 +548,7 @@ class UserSelect(BaseSelect[V]):
max_values: int = 1, max_values: int = 1,
disabled: bool = False, disabled: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None: ) -> None:
super().__init__( super().__init__(
self.type, self.type,
@ -435,6 +558,7 @@ class UserSelect(BaseSelect[V]):
max_values=max_values, max_values=max_values,
disabled=disabled, disabled=disabled,
row=row, row=row,
default_values=_handle_select_defaults(default_values, self.type),
) )
@property @property
@ -455,6 +579,18 @@ class UserSelect(BaseSelect[V]):
""" """
return super().values # type: ignore 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]): class RoleSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current roles of the guild. """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. Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. 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`] row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5 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 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). ordering. The row number must be between 0 and 4 (i.e. zero indexed).
""" """
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__( def __init__(
self, self,
*, *,
@ -495,6 +637,7 @@ class RoleSelect(BaseSelect[V]):
max_values: int = 1, max_values: int = 1,
disabled: bool = False, disabled: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None: ) -> None:
super().__init__( super().__init__(
self.type, self.type,
@ -504,6 +647,7 @@ class RoleSelect(BaseSelect[V]):
max_values=max_values, max_values=max_values,
disabled=disabled, disabled=disabled,
row=row, row=row,
default_values=_handle_select_defaults(default_values, self.type),
) )
@property @property
@ -516,6 +660,18 @@ class RoleSelect(BaseSelect[V]):
"""List[:class:`discord.Role`]: A list of roles that have been selected by the user.""" """List[:class:`discord.Role`]: A list of roles that have been selected by the user."""
return super().values # type: ignore 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]): class MentionableSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current members and roles in the guild. """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. Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. 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`] row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5 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 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). ordering. The row number must be between 0 and 4 (i.e. zero indexed).
""" """
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
def __init__( def __init__(
self, self,
*, *,
@ -559,6 +722,7 @@ class MentionableSelect(BaseSelect[V]):
max_values: int = 1, max_values: int = 1,
disabled: bool = False, disabled: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None: ) -> None:
super().__init__( super().__init__(
self.type, self.type,
@ -568,6 +732,7 @@ class MentionableSelect(BaseSelect[V]):
max_values=max_values, max_values=max_values,
disabled=disabled, disabled=disabled,
row=row, row=row,
default_values=_handle_select_defaults(default_values, self.type),
) )
@property @property
@ -588,6 +753,18 @@ class MentionableSelect(BaseSelect[V]):
""" """
return super().values # type: ignore 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]): class ChannelSelect(BaseSelect[V]):
"""Represents a UI select menu with a list of predefined options with the current channels in the guild. """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. Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. 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`] row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5 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 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). 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__( def __init__(
self, self,
@ -633,6 +817,7 @@ class ChannelSelect(BaseSelect[V]):
max_values: int = 1, max_values: int = 1,
disabled: bool = False, disabled: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
default_values: Sequence[ValidDefaultValues] = MISSING,
) -> None: ) -> None:
super().__init__( super().__init__(
self.type, self.type,
@ -643,6 +828,7 @@ class ChannelSelect(BaseSelect[V]):
disabled=disabled, disabled=disabled,
row=row, row=row,
channel_types=channel_types, channel_types=channel_types,
default_values=_handle_select_defaults(default_values, self.type),
) )
@property @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.""" """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 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 @overload
def select( def select(
@ -697,6 +895,7 @@ def select(
min_values: int = ..., min_values: int = ...,
max_values: int = ..., max_values: int = ...,
disabled: bool = ..., disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ..., row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, UserSelectT]: ) -> SelectCallbackDecorator[V, UserSelectT]:
... ...
@ -713,6 +912,7 @@ def select(
min_values: int = ..., min_values: int = ...,
max_values: int = ..., max_values: int = ...,
disabled: bool = ..., disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ..., row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, RoleSelectT]: ) -> SelectCallbackDecorator[V, RoleSelectT]:
... ...
@ -729,6 +929,7 @@ def select(
min_values: int = ..., min_values: int = ...,
max_values: int = ..., max_values: int = ...,
disabled: bool = ..., disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ..., row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, ChannelSelectT]: ) -> SelectCallbackDecorator[V, ChannelSelectT]:
... ...
@ -745,6 +946,7 @@ def select(
min_values: int = ..., min_values: int = ...,
max_values: int = ..., max_values: int = ...,
disabled: bool = ..., disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
row: Optional[int] = ..., row: Optional[int] = ...,
) -> SelectCallbackDecorator[V, MentionableSelectT]: ) -> SelectCallbackDecorator[V, MentionableSelectT]:
... ...
@ -760,6 +962,7 @@ def select(
min_values: int = 1, min_values: int = 1,
max_values: int = 1, max_values: int = 1,
disabled: bool = False, disabled: bool = False,
default_values: Sequence[ValidDefaultValues] = MISSING,
row: Optional[int] = None, row: Optional[int] = None,
) -> SelectCallbackDecorator[V, BaseSelectT]: ) -> SelectCallbackDecorator[V, BaseSelectT]:
"""A decorator that attaches a select menu to a component. """A decorator that attaches a select menu to a component.
@ -831,6 +1034,11 @@ def select(
with :class:`ChannelSelect` instances. with :class:`ChannelSelect` instances.
disabled: :class:`bool` disabled: :class:`bool`
Whether the select is disabled or not. Defaults to ``False``. 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]: def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]:
@ -838,8 +1046,8 @@ def select(
raise TypeError('select function must be a coroutine function') raise TypeError('select function must be a coroutine function')
callback_cls = getattr(cls, '__origin__', cls) callback_cls = getattr(cls, '__origin__', cls)
if not issubclass(callback_cls, BaseSelect): if not issubclass(callback_cls, BaseSelect):
supported_classes = ", ".join(["ChannelSelect", "MentionableSelect", "RoleSelect", "Select", "UserSelect"]) 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}.') 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_type__ = callback_cls
func.__discord_ui_model_kwargs__ = { func.__discord_ui_model_kwargs__ = {
@ -854,6 +1062,24 @@ def select(
func.__discord_ui_model_kwargs__['options'] = options func.__discord_ui_model_kwargs__['options'] = options
if issubclass(callback_cls, ChannelSelect): if issubclass(callback_cls, ChannelSelect):
func.__discord_ui_model_kwargs__['channel_types'] = channel_types 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 return func

94
discord/ui/view.py

@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations 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 functools import partial
from itertools import groupby from itertools import groupby
@ -33,6 +33,7 @@ import sys
import time import time
import os import os
from .item import Item, ItemCallbackType from .item import Item, ItemCallbackType
from .dynamic import DynamicItem
from ..components import ( from ..components import (
Component, Component,
ActionRow as ActionRowComponent, ActionRow as ActionRowComponent,
@ -50,6 +51,7 @@ __all__ = (
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
import re
from ..interactions import Interaction from ..interactions import Interaction
from ..message import Message from ..message import Message
@ -76,9 +78,10 @@ def _component_to_item(component: Component) -> Item:
return Button.from_component(component) return Button.from_component(component)
if isinstance(component, SelectComponent): 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) return Item.from_component(component)
@ -417,7 +420,7 @@ class View:
try: try:
item._refresh_state(interaction, interaction.data) # type: ignore 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: if not allow:
return return
@ -534,6 +537,8 @@ class ViewStore:
self._synced_message_views: Dict[int, View] = {} self._synced_message_views: Dict[int, View] = {}
# custom_id: Modal # custom_id: Modal
self._modals: Dict[str, Modal] = {} self._modals: Dict[str, Modal] = {}
# component_type is the key
self._dynamic_items: Dict[re.Pattern[str], Type[DynamicItem[Item[Any]]]] = {}
self._state: ConnectionState = state self._state: ConnectionState = state
@property @property
@ -548,6 +553,16 @@ class ViewStore:
# fmt: on # fmt: on
return list(views.values()) 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: def add_view(self, view: View, message_id: Optional[int] = None) -> None:
view._start_listening_from_store(self) view._start_listening_from_store(self)
if view.__discord_ui_modal__: if view.__discord_ui_modal__:
@ -555,12 +570,17 @@ class ViewStore:
return return
dispatch_info = self._views.setdefault(message_id, {}) dispatch_info = self._views.setdefault(message_id, {})
is_fully_dynamic = True
for item in view._children: 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 dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
is_fully_dynamic = False
view._cache_key = message_id 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 self._synced_message_views[message_id] = view
def remove_view(self, view: View) -> None: def remove_view(self, view: View) -> None:
@ -571,7 +591,10 @@ class ViewStore:
dispatch_info = self._views.get(view._cache_key) dispatch_info = self._views.get(view._cache_key)
if dispatch_info: if dispatch_info:
for item in view._children: for item in view._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 dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
if len(dispatch_info) == 0: if len(dispatch_info) == 0:
@ -579,7 +602,64 @@ class ViewStore:
self._synced_message_views.pop(view._cache_key, None) # type: ignore 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: 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 interaction_id: Optional[int] = None
message_id: Optional[int] = None message_id: Optional[int] = None
# Realistically, in a component based interaction the Interaction.message will never be 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_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) _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])' _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: 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. The text with the markdown special characters removed.
""" """
def replacement(match): def replacement(match: re.Match[str]) -> str:
groupdict = match.groupdict() groupdict = match.groupdict()
return groupdict.get('url', '') return groupdict.get('url', '')
@ -1380,3 +1380,17 @@ CAMEL_CASE_REGEX = re.compile(r'(?<!^)(?=[A-Z])')
def _to_kebab_case(text: str) -> str: def _to_kebab_case(text: str) -> str:
return CAMEL_CASE_REGEX.sub('-', text).lower() 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 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 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. 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 from __future__ import annotations
import asyncio import asyncio
import socket
import logging import logging
import struct import struct
import threading
from typing import Any, Callable, List, Optional, TYPE_CHECKING, Tuple, Union from typing import Any, Callable, List, Optional, TYPE_CHECKING, Tuple, Union
from . import opus, utils from . import opus
from .backoff import ExponentialBackoff
from .gateway import * from .gateway import *
from .errors import ClientException, ConnectionClosed from .errors import ClientException
from .player import AudioPlayer, AudioSource from .player import AudioPlayer, AudioSource
from .utils import MISSING from .utils import MISSING
from .voice_state import VoiceConnectionState
if TYPE_CHECKING: if TYPE_CHECKING:
from .gateway import DiscordVoiceWebSocket
from .client import Client from .client import Client
from .guild import Guild from .guild import Guild
from .state import ConnectionState from .state import ConnectionState
from .user import ClientUser from .user import ClientUser
from .opus import Encoder from .opus import Encoder, APPLICATION_CTL, BAND_CTL, SIGNAL_CTL
from .channel import StageChannel, VoiceChannel from .channel import StageChannel, VoiceChannel
from . import abc from . import abc
@ -226,12 +210,6 @@ class VoiceClient(VoiceProtocol):
""" """
channel: VocalGuildChannel 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: def __init__(self, client: Client, channel: abc.Connectable) -> None:
if not has_nacl: if not has_nacl:
@ -239,29 +217,18 @@ class VoiceClient(VoiceProtocol):
super().__init__(client, channel) super().__init__(client, channel)
state = client._connection state = client._connection
self.token: str = MISSING
self.server_id: int = MISSING self.server_id: int = MISSING
self.socket = MISSING self.socket = MISSING
self.loop: asyncio.AbstractEventLoop = state.loop self.loop: asyncio.AbstractEventLoop = state.loop
self._state: ConnectionState = state 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.sequence: int = 0
self.timestamp: int = 0 self.timestamp: int = 0
self.timeout: float = 0
self._runner: asyncio.Task = MISSING
self._player: Optional[AudioPlayer] = None self._player: Optional[AudioPlayer] = None
self.encoder: Encoder = MISSING self.encoder: Encoder = MISSING
self._lite_nonce: int = 0 self._lite_nonce: int = 0
self.ws: DiscordVoiceWebSocket = MISSING
self._connection: VoiceConnectionState = self.create_connection_state()
warn_nacl: bool = not has_nacl warn_nacl: bool = not has_nacl
supported_modes: Tuple[SupportedModes, ...] = ( supported_modes: Tuple[SupportedModes, ...] = (
@ -280,6 +247,38 @@ class VoiceClient(VoiceProtocol):
""":class:`ClientUser`: The user connected to voice (i.e. ourselves).""" """:class:`ClientUser`: The user connected to voice (i.e. ourselves)."""
return self._state.user # type: ignore 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: def checked_add(self, attr: str, value: int, limit: int) -> None:
val = getattr(self, attr) val = getattr(self, attr)
if val + value > limit: if val + value > limit:
@ -289,144 +288,23 @@ class VoiceClient(VoiceProtocol):
# connection related # connection related
def create_connection_state(self) -> VoiceConnectionState:
return VoiceConnectionState(self)
async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None: async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None:
self.session_id: str = data['session_id'] await self._connection.voice_state_update(data)
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()
async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None: async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
if self._voice_server_complete.is_set(): await self._connection.voice_server_update(data)
_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
async def connect(self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False) -> None: async def connect(self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False) -> None:
_log.info('Connecting to voice...') await self._connection.connect(
self.timeout = timeout reconnect=reconnect, timeout=timeout, self_deaf=self_deaf, self_mute=self_mute, resume=False
)
for i in range(5):
self.prepare_handshake() def wait_until_connected(self, timeout: Optional[float] = 30.0) -> bool:
self._connection.wait(timeout)
# This has to be created before we start the flow. return self._connection.is_connected()
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
@property @property
def latency(self) -> float: def latency(self) -> float:
@ -437,7 +315,7 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4 .. versionadded:: 1.4
""" """
ws = self.ws ws = self._connection.ws
return float("inf") if not ws else ws.latency return float("inf") if not ws else ws.latency
@property @property
@ -446,72 +324,19 @@ class VoiceClient(VoiceProtocol):
.. versionadded:: 1.4 .. versionadded:: 1.4
""" """
ws = self.ws ws = self._connection.ws
return float("inf") if not ws else ws.average_latency return float("inf") if not ws else ws.average_latency
async def 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: async def disconnect(self, *, force: bool = False) -> None:
"""|coro| """|coro|
Disconnects this voice client from voice. Disconnects this voice client from voice.
""" """
if not force and not self.is_connected():
return
self.stop() self.stop()
self._connected.clear() await self._connection.disconnect(force=force)
self.cleanup()
try: async def move_to(self, channel: Optional[abc.Snowflake], *, timeout: Optional[float] = 30.0) -> None:
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:
"""|coro| """|coro|
Moves you to a different voice channel. Moves you to a different voice channel.
@ -520,12 +345,21 @@ class VoiceClient(VoiceProtocol):
----------- -----------
channel: Optional[:class:`abc.Snowflake`] channel: Optional[:class:`abc.Snowflake`]
The channel to move to. Must be a voice channel. 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: def is_connected(self) -> bool:
"""Indicates if the voice client is connected to voice.""" """Indicates if the voice client is connected to voice."""
return self._connected.is_set() return self._connection.is_connected()
# audio related # audio related
@ -564,7 +398,18 @@ class VoiceClient(VoiceProtocol):
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] 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`. """Plays an :class:`AudioSource`.
The finalizer, ``after`` is called after the source has been exhausted 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 caught and the audio player is then stopped. If no after callback is
passed, any caught exception will be logged using the library logger. 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 .. versionchanged:: 2.0
Instead of writing to ``sys.stderr``, the library's logger is used. Instead of writing to ``sys.stderr``, the library's logger is used.
.. versionchanged:: 2.4
Added encoder parameters as keyword arguments.
Parameters Parameters
----------- -----------
source: :class:`AudioSource` source: :class:`AudioSource`
@ -585,6 +436,27 @@ class VoiceClient(VoiceProtocol):
The finalizer that is called after the stream is exhausted. The finalizer that is called after the stream is exhausted.
This function must have a single parameter, ``error``, that This function must have a single parameter, ``error``, that
denotes an optional exception that was raised during playing. 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 Raises
------- -------
@ -594,6 +466,8 @@ class VoiceClient(VoiceProtocol):
Source is not a :class:`AudioSource` or after is not a callable. Source is not a :class:`AudioSource` or after is not a callable.
OpusNotLoaded OpusNotLoaded
Source is not opus encoded and opus is not loaded. 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(): if not self.is_connected():
@ -605,8 +479,15 @@ class VoiceClient(VoiceProtocol):
if not isinstance(source, AudioSource): if not isinstance(source, AudioSource):
raise TypeError(f'source must be an AudioSource not {source.__class__.__name__}') raise TypeError(f'source must be an AudioSource not {source.__class__.__name__}')
if not self.encoder and not source.is_opus(): if not source.is_opus():
self.encoder = opus.Encoder() 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 = AudioPlayer(source, self, after=after)
self._player.start() self._player.start()
@ -651,7 +532,7 @@ class VoiceClient(VoiceProtocol):
if self._player is None: if self._player is None:
raise ValueError('Not playing anything.') 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: def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None:
"""Sends an audio packet composed of the data. """Sends an audio packet composed of the data.
@ -680,8 +561,8 @@ class VoiceClient(VoiceProtocol):
encoded_data = data encoded_data = data
packet = self._get_voice_packet(encoded_data) packet = self._get_voice_packet(encoded_data)
try: try:
self.socket.sendto(packet, (self.endpoint_ip, self.voice_port)) self._connection.send_packet(packet)
except BlockingIOError: except OSError:
_log.warning('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) _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) 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; box-sizing: border-box;
} }
section {
word-break: break-word;
}
/* CSS variables would go here */ /* CSS variables would go here */
:root { :root {
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; --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-entry-hover-text: var(--blue-2);
--attribute-table-badge: var(--grey-7); --attribute-table-badge: var(--grey-7);
--highlighted-text: rgb(252, 233, 103); --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"] { :root[data-font="serif"] {
@ -216,6 +226,7 @@ body {
display: grid; display: grid;
min-height: 100%; min-height: 100%;
grid-auto-rows: min-content auto min-content; grid-auto-rows: min-content auto min-content;
grid-template-columns: minmax(0, 1fr);
grid-template-areas: grid-template-areas:
"s" "s"
"h" "h"
@ -1046,6 +1057,7 @@ code.xref, a code {
span.pre { span.pre {
padding: 0 2px; padding: 0 2px;
white-space: pre-wrap !important;
} }
dl.class { dl.class {
@ -1211,12 +1223,13 @@ div.code-block-caption {
/* desktop stuff */ /* desktop stuff */
@media screen and (min-width: 600px) { @media screen and (min-width: 768px) {
.grid-item { .grid-item {
max-width: unset; max-width: unset;
} }
.main-grid { .main-grid {
grid-template-columns: repeat(6, 1fr);
grid-template-areas: grid-template-areas:
"h h h h h h" "h h h h h h"
"n n n n n n" "n n n n n n"
@ -1273,6 +1286,7 @@ div.code-block-caption {
position: sticky; position: sticky;
top: 1em; top: 1em;
max-height: calc(100vh - 2em); max-height: calc(100vh - 2em);
max-width: 100%;
overflow-y: auto; overflow-y: auto;
margin: 1em; 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" "s s s f f f f f f f f f f f f f"
} }
#sidebar {
max-width: unset;
}
header > nav { header > nav {
margin-left: 18.75%; margin-left: 18.75%;
margin-right: 18.75%; margin-right: 18.75%;

285
docs/api.rst

@ -496,6 +496,47 @@ Debug
:type payload: Union[:class:`bytes`, :class:`str`] :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 Gateway
~~~~~~~~ ~~~~~~~~
@ -835,7 +876,7 @@ Members
.. function:: on_member_ban(guild, user) .. 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. 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 Consider using :func:`on_raw_reaction_add` if you need this and do not otherwise want
to enable the members intent. 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. :param reaction: The current state of the reaction.
:type reaction: :class:`Reaction` :type reaction: :class:`Reaction`
:param user: The user who added the 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 Consider using :func:`on_raw_reaction_remove` if you need this and do not want
to enable the members intent. 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. :param reaction: The current state of the reaction.
:type reaction: :class:`Reaction` :type reaction: :class:`Reaction`
:param user: The user whose reaction was removed. :param user: The user whose reaction was removed.
@ -1513,6 +1566,12 @@ of :class:`enum.Enum`.
.. versionadded:: 2.0 .. versionadded:: 2.0
.. attribute:: media
A media channel.
.. versionadded:: 2.4
.. class:: MessageType .. class:: MessageType
Specifies the type of :class:`Message`. This is used to denote if a message 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 When this is the action, the type of :attr:`~AuditLogEntry.target` is
the :class:`User` or :class:`Object` who got kicked. 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. When this is the action, :attr:`~AuditLogEntry.changes` is empty.
.. attribute:: member_prune .. attribute:: member_prune
@ -2160,6 +2224,11 @@ of :class:`enum.Enum`.
When this is the action, the type of :attr:`~AuditLogEntry.target` is When this is the action, the type of :attr:`~AuditLogEntry.target` is
the :class:`Member`, :class:`User`, or :class:`Object` who got the role. 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`: Possible attributes for :class:`AuditLogDiff`:
- :attr:`~AuditLogDiff.roles` - :attr:`~AuditLogDiff.roles`
@ -2799,6 +2868,18 @@ of :class:`enum.Enum`.
.. versionadded:: 2.1 .. 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 .. attribute:: onboarding_question_create
A guild onboarding prompt was created. A guild onboarding prompt was created.
@ -2873,6 +2954,27 @@ of :class:`enum.Enum`.
Represents a member currently in the team. 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 .. class:: WebhookType
Represents the type of webhook that can be received. 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 The rule will trigger when combined number of role and user mentions
is greater than the set limit. 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 .. class:: AutoModRuleEventType
Represents the event type of an automod rule. 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. 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 .. class:: AutoModRuleActionType
Represents the action type of an automod rule. Represents the action type of an automod rule.
@ -3339,6 +3453,12 @@ of :class:`enum.Enum`.
The rule will timeout a user. 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 .. class:: ForumLayoutType
@ -3373,6 +3493,65 @@ of :class:`enum.Enum`.
Sort forum posts by creation time (from most recent to oldest). 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 .. class:: OnboardingPromptType
@ -4024,6 +4203,12 @@ AuditLogDiff
The trigger for the automod rule. 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` :type: :class:`AutoModTrigger`
.. attribute:: actions .. attribute:: actions
@ -4110,7 +4295,63 @@ AuditLogDiff
See also :attr:`ForumChannel.default_reaction_emoji` 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 .. attribute:: options
@ -4784,6 +5025,22 @@ ShardInfo
.. autoclass:: ShardInfo() .. autoclass:: ShardInfo()
:members: :members:
SKU
~~~~~~~~~~~
.. attributetable:: SKU
.. autoclass:: SKU()
:members:
Entitlement
~~~~~~~~~~~
.. attributetable:: Entitlement
.. autoclass:: Entitlement()
:members:
RawMessageDeleteEvent RawMessageDeleteEvent
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
@ -5137,6 +5394,30 @@ MemberFlags
.. autoclass:: MemberFlags .. autoclass:: MemberFlags
:members: :members:
AttachmentFlags
~~~~~~~~~~~~~~~~
.. attributetable:: AttachmentFlags
.. autoclass:: AttachmentFlags
:members:
RoleFlags
~~~~~~~~~~
.. attributetable:: RoleFlags
.. autoclass:: RoleFlags
:members:
SKUFlags
~~~~~~~~~~~
.. attributetable:: SKUFlags
.. autoclass:: SKUFlags()
:members:
ForumTag ForumTag
~~~~~~~~~ ~~~~~~~~~

1
docs/conf.py

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

17
docs/interactions/api.rst

@ -166,6 +166,14 @@ SelectOption
.. autoclass:: SelectOption .. autoclass:: SelectOption
:members: :members:
SelectDefaultValue
~~~~~~~~~~~~~~~~~~~
.. attributetable:: SelectDefaultValue
.. autoclass:: SelectDefaultValue
:members:
Choice Choice
~~~~~~~ ~~~~~~~
@ -443,6 +451,15 @@ Item
.. autoclass:: discord.ui.Item .. autoclass:: discord.ui.Item
:members: :members:
DynamicItem
~~~~~~~~~~~~
.. attributetable:: discord.ui.DynamicItem
.. autoclass:: discord.ui.DynamicItem
:members:
:inherited-members:
Button 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 "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -30,12 +30,12 @@ msgid "Creating a Bot account is a pretty straightforward process."
msgstr "Botのアカウント作成はとても簡単です。" msgstr "Botのアカウント作成はとても簡単です。"
#: ../../discord.rst:12 #: ../../discord.rst:12
#: ../../discord.rst:66 #: ../../discord.rst:61
msgid "Make sure you're logged on to the `Discord website <https://discord.com>`_." msgid "Make sure you're logged on to the `Discord website <https://discord.com>`_."
msgstr "`Discordのウェブサイト <https://discord.com>`_ にログインできていることを確認してください。" msgstr "`Discordのウェブサイト <https://discord.com>`_ にログインできていることを確認してください。"
#: ../../discord.rst:13 #: ../../discord.rst:13
#: ../../discord.rst:67 #: ../../discord.rst:62
msgid "Navigate to the `application page <https://discord.com/developers/applications>`_" msgid "Navigate to the `application page <https://discord.com/developers/applications>`_"
msgstr "`Applicationページ <https://discord.com/developers/applications>`_ に移動します。" msgstr "`Applicationページ <https://discord.com/developers/applications>`_ に移動します。"
@ -56,22 +56,14 @@ msgid "The new application form filled in."
msgstr "記入された新しいアプリケーションフォーム" msgstr "記入された新しいアプリケーションフォーム"
#: ../../discord.rst:24 #: ../../discord.rst:24
msgid "Create a Bot User by navigating to the \"Bot\" tab and clicking \"Add Bot\"." msgid "Navigate to the \"Bot\" tab to configure it."
msgstr "「Bot」タブへ移動し、「Add Bot」をクリックしてBotユーザーを作成します。" msgstr ""
#: ../../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」ボタン"
#: ../../discord.rst:30 #: ../../discord.rst:25
msgid "Make sure that **Public Bot** is ticked if you want others to invite your bot." msgid "Make sure that **Public Bot** is ticked if you want others to invite your bot."
msgstr "他人にBotの招待を許可する場合には、 **Public 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**." 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** がオフになっていることを確認する必要があります。わからない場合は **チェックを外してください** 。" msgstr "また、必要なサービスを開発している場合を除いて、 **Require OAuth2 Code Grant** がオフになっていることを確認する必要があります。わからない場合は **チェックを外してください** 。"
@ -79,55 +71,55 @@ msgstr "また、必要なサービスを開発している場合を除いて、
msgid "How the Bot User options should look like for most people." msgid "How the Bot User options should look like for most people."
msgstr "Botユーザーの設定がほとんどの人にとってどのように見えるか" msgstr "Botユーザーの設定がほとんどの人にとってどのように見えるか"
#: ../../discord.rst:38 #: ../../discord.rst:33
msgid "Copy the token using the \"Copy\" button." msgid "Copy the token using the \"Copy\" button."
msgstr "「Copy」ボタンを使ってトークンをコピーします。" msgstr "「Copy」ボタンを使ってトークンをコピーします。"
#: ../../discord.rst:40 #: ../../discord.rst:35
msgid "**This is not the Client Secret at the General Information page.**" msgid "**This is not the Client Secret at the General Information page.**"
msgstr "**General InformationページのClient Secretではないので注意してください。**" 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." 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したり、すべての人にメンションを送るなどといった悪質な行為を行える様になってしまいます。" msgstr "このトークンは、あなたのBotのパスワードと同義であることを覚えておきましょう。誰か他の人とトークンを共有することは絶対に避けてください。トークンがあれば、誰かがあなたのBotにログインし、サーバーから退出したり、サーバー内のすべてのメンバーをBANしたり、すべての人にメンションを送るなどといった悪質な行為を行える様になってしまいます。"
#: ../../discord.rst:49 #: ../../discord.rst:44
msgid "The possibilities are endless, so **do not share this token.**" msgid "The possibilities are endless, so **do not share this token.**"
msgstr "可能性は無限にあるので、絶対に **トークンを共有しないでください** 。" 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." 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」ボタンをクリックしましょう。これによって古いトークンが無効になり、新しいトークンが再生成されます。今度からは新しいトークンを利用してログインを行う必要があります。" 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." msgid "And that's it. You now have a bot account and you can login with that token."
msgstr "以上です。 これでボットアカウントが作成され、そのトークンでログインできます。" msgstr "以上です。 これでボットアカウントが作成され、そのトークンでログインできます。"
#: ../../discord.rst:60 #: ../../discord.rst:55
msgid "Inviting Your Bot" msgid "Inviting Your Bot"
msgstr "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." msgid "So you've made a Bot User but it's not actually in any server."
msgstr "Botのユーザーを作成しましたが、現時点ではどのサーバーにも参加していない状態です。" msgstr "Botのユーザーを作成しましたが、現時点ではどのサーバーにも参加していない状態です。"
#: ../../discord.rst:64 #: ../../discord.rst:59
msgid "If you want to invite your bot you must create an invite URL for it." msgid "If you want to invite your bot you must create an invite URL for it."
msgstr "Botを招待したい場合は、そのための招待URLを作成する必要があります。" msgstr "Botを招待したい場合は、そのための招待URLを作成する必要があります。"
#: ../../discord.rst:68 #: ../../discord.rst:63
msgid "Click on your bot's page." msgid "Click on your bot's page."
msgstr "Botのページを開きます。" msgstr "Botのページを開きます。"
#: ../../discord.rst:69 #: ../../discord.rst:64
msgid "Go to the \"OAuth2\" tab." msgid "Go to the \"OAuth2 > URL Generator\" tab."
msgstr "「OAuth2」タブへ移動します。" msgstr ""
#: ../../discord.rst:0 #: ../../discord.rst:0
msgid "How the OAuth2 page should look like." msgid "How the OAuth2 page should look like."
msgstr "OAuth2ページがどのように見えるか" msgstr "OAuth2ページがどのように見えるか"
#: ../../discord.rst:74 #: ../../discord.rst:69
msgid "Tick the \"bot\" checkbox under \"scopes\"." msgid "Tick the \"bot\" checkbox under \"scopes\"."
msgstr "「scopes」下にある「bot」チェックボックスを選択してください。" msgstr "「scopes」下にある「bot」チェックボックスを選択してください。"
@ -135,15 +127,15 @@ msgstr "「scopes」下にある「bot」チェックボックスを選択して
msgid "The scopes checkbox with \"bot\" ticked." msgid "The scopes checkbox with \"bot\" ticked."
msgstr "「bot」がチェックされたスコープのチェックボックス" msgstr "「bot」がチェックされたスコープのチェックボックス"
#: ../../discord.rst:79 #: ../../discord.rst:74
msgid "Tick the permissions required for your bot to function under \"Bot Permissions\"." msgid "Tick the permissions required for your bot to function under \"Bot Permissions\"."
msgstr "「Bot Permissions」からBotの機能に必要な権限を選択してください。" 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." msgid "Please be aware of the consequences of requiring your bot to have the \"Administrator\" permission."
msgstr "Botに「管理者」権限を要求させることによる影響は認識しておきましょう。" 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." 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>`_ を参照してください。" 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." msgid "The permission checkboxes with some permissions checked."
msgstr "いくつかの権限にチェックが入った権限のチェックボックス" 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\"." 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を招待したいサーバーを選択した後、「認証」をクリックしてください。" msgstr "結果的に生成されたURLを使ってBotをサーバーに追加することができます。URLをコピーしてブラウザに貼り付け、Botを招待したいサーバーを選択した後、「認証」をクリックしてください。"
#: ../../discord.rst:93 #: ../../discord.rst:88
msgid "The person adding the bot needs \"Manage Server\" permissions to do so." msgid "The person adding the bot needs \"Manage Server\" permissions to do so."
msgstr "Botを追加する人には「サーバー管理」権限が必要です。" 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`." 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` を使用できます。" 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 "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-29 20:45+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -581,8 +581,8 @@ msgid "A view was not passed."
msgstr "Viewが渡されなかった" msgstr "Viewが渡されなかった"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.add_view:16 #: ../../../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." 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 "Viewは永続的ではありません。永続的なViewにはタイムアウトがなく、すべてのコンポーネントには明示的に渡された custom_id があります" msgstr ""
#: ../../../discord/ext/commands/bot.py:docstring of discord.ext.commands.Bot.allowed_mentions:1 #: ../../../discord/ext/commands/bot.py:docstring of discord.ext.commands.Bot.allowed_mentions:1
msgid "The allowed mention configuration." 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` を取得します。" msgstr "Botが所属するGuildを取得できる、 :term:`asynchronous iterator` を取得します。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:5 #: ../../../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`." 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 "これを使った場合、各 :class:`Guild` の :attr:`Guild.owner` 、 :attr:`Guild.icon` 、 :attr:`Guild.id` 、 :attr:`Guild.name` のみ取得できます。" 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." msgid "This method is an API call. For general usage, consider :attr:`guilds` instead."
msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わりに使用してください。" 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/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:37
#: ../../../discord/ext/commands/core.py:docstring of discord.ext.commands.core.check_any:20 #: ../../../discord/ext/commands/core.py:docstring of discord.ext.commands.core.check_any:20
@ -947,37 +947,41 @@ msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わ
msgid "Examples" msgid "Examples"
msgstr "例" 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 #: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.history:7
msgid "Usage ::" msgid "Usage ::"
msgstr "使い方 ::" 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 ::" msgid "Flattening into a list ::"
msgstr "リストへフラット化 ::" 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 #: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.history:19
msgid "All parameters are optional." msgid "All parameters are optional."
msgstr "すべてのパラメータがオプションです。" 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``." 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です。" 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." msgid "The default has been changed to 200."
msgstr "デフォルトが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." 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である場合、これはローカル時間であるとみなされます。" 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." 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」である場合、これは地域時間であるとみなされます。" msgstr "渡された日付、またはオブジェクトより後のギルドを取得します。日付を指定する場合、UTC対応の「aware」を利用することを推奨します。日付が「naive」である場合、これは地域時間であるとみなされます。"
#: ../../../discord/ext/commands/bot.py:docstring of discord.client.Client.fetch_guilds:44 #: ../../../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." msgid "Getting the guilds failed."
msgstr "Guildの取得に失敗した場合。" msgstr "Guildの取得に失敗した場合。"
@ -989,7 +993,7 @@ msgstr "Guildの取得に失敗した場合。"
msgid "Yields" msgid "Yields"
msgstr "Yieldする値" 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." msgid ":class:`.Guild` -- The guild with the guild data parsed."
msgstr ":class:`.Guild` -- データを解析したGuild。" 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/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_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." msgid "If the function is not a coroutine or is already a command."
msgstr "関数がコルーチンでない場合、またはすでにコマンドが登録されている場合。" 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` を取ります。" 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_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." msgid "Whether to register the command also as an application command."
msgstr "アプリケーションコマンドとしてもコマンドを登録するかどうか。" 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." 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` デコレータに似ていますが、代わりにハイブリッドグループを作成します。" 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 #: ../../ext/commands/api.rst:131
msgid "Command" msgid "Command"
msgstr "Command" msgstr "Command"
@ -2925,7 +2933,11 @@ msgstr "コグが削除された際に呼び出される特別なメソッド。
msgid "Subclasses must replace this if they want special unloading behaviour." msgid "Subclasses must replace this if they want special unloading behaviour."
msgstr "サブクラスは特別なアンロード動作が必要な場合にこれを置き換えなければなりません。" 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`." msgid "This method can now be a :term:`coroutine`."
msgstr "このメソッドは現在 :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." 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` として登録する特別なメソッド。" 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 #: ../../../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." msgid "A special method that is called whenever an error is dispatched inside this cog."
msgstr "このコグ内でエラーが発生するたびに呼び出される特別なメソッド。" 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` のようなデコレータはコグの上に使用されている場合、グループに適用されます。" 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 #: ../../../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." 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 "グループにもハイブリッドコマンドが追加され、プレフィックス形式のコマンドをルートレベルのコマンドとして保持しながら、スラッシュコマンドをグループに分類できるようになります。" msgstr ""
#: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.GroupCog:14 #: ../../../discord/ext/commands/cog.py:docstring of discord.ext.commands.cog.GroupCog:14
msgid "For example:" 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: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.DefaultHelpCommand:42
#: ../../../discord/ext/commands/help.py:docstring of discord.ext.commands.help.Paginator:25 #: ../../../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`" msgid ":class:`int`"
msgstr ":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." msgid "Returns the cog associated with this context's command. None if it does not exist."
msgstr "このコンテキストのコマンドに関連付けられたコグを返します。存在しない場合はNoneを返します。" 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 #: ../../docstring of discord.ext.commands.Context.guild:1
msgid "Returns the guild associated with this context's command. None if not available." msgid "Returns the guild associated with this context's command. None if not available."
msgstr "このコンテキストのコマンドに関連付けられているギルドを返します。利用できない場合はNoneを返します。" msgstr "このコンテキストのコマンドに関連付けられているギルドを返します。利用できない場合はNoneを返します。"
@ -4496,72 +4522,6 @@ msgstr "ヘルプを表示するエンティティ。"
msgid "The result of the help command, if any." msgid "The result of the help command, if any."
msgstr "もしあれば、ヘルプコマンドの結果。" 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 #: ../../../discord/ext/commands/context.py:docstring of discord.abc.Messageable.fetch_message:3
msgid "Retrieves a single :class:`~discord.Message` from the destination." msgid "Retrieves a single :class:`~discord.Message` from the destination."
msgstr "出力先から、単一の :class:`~discord.Message` を取得します。" msgstr "出力先から、単一の :class:`~discord.Message` を取得します。"
@ -4586,6 +4546,12 @@ msgstr "メッセージの取得に失敗した場合。"
msgid "The message asked for." msgid "The message asked for."
msgstr "要求されたメッセージ。" 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 #: ../../../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." msgid "Returns an :term:`asynchronous iterator` that enables receiving the destination's message history."
msgstr "出力先のメッセージ履歴を取得する :term:`asynchronous iterator` を返します。" msgstr "出力先のメッセージ履歴を取得する :term:`asynchronous iterator` を返します。"
@ -4654,6 +4620,66 @@ msgstr "現時点でピン留めされているメッセージ。"
msgid "List[:class:`~discord.Message`]" msgid "List[:class:`~discord.Message`]"
msgstr "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 #: ../../../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." msgid "Sends a message to the destination with the content given."
msgstr "指定された内容のメッセージを出力先に送信します。" 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**." 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分に設定されます。 **これはインタラクションベースのコンテキストでのみ適用されます。**" 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." msgid "The ``files`` list is not of the appropriate size."
msgstr "``files`` リストの大きさが適切でない場合。" 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`." 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` でない場合。" 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.MemberConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:9 #: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:9
msgid "Lookup by name#discrim" msgid "Lookup by username#discriminator (deprecated)."
msgstr "名前#タグ で検索" 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.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.UserConverter:10
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.TextChannelConverter:10 msgid "Lookup by username#0 (deprecated, only gets users that migrated from their discriminator)."
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.VoiceChannelConverter:10 msgstr ""
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.StageChannelConverter:12
msgid "Lookup by name"
msgstr "名前 で検索"
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:12 #: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.MemberConverter:12
msgid "Lookup by nickname" msgid "Lookup by guild nickname."
msgstr "ニックネーム で検索" 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.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`" msgid "Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`"
msgstr "一般的な :exc:`.BadArgument` の代わりに :exc:`.MemberNotFound` を発生させます。" 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." 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` が有効な場合には結果がキャッシュされるようになりました。" 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 #: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.UserConverter:1
msgid "Converts to a :class:`~discord.User`." msgid "Converts to a :class:`~discord.User`."
msgstr ":class:`~discord.User` に変換します。" msgstr ":class:`~discord.User` に変換します。"
@ -4895,11 +4937,11 @@ msgstr ":class:`~discord.User` に変換します。"
msgid "All lookups are via the global user cache." msgid "All lookups are via the global user cache."
msgstr "すべての検索はグローバルユーザーキャッシュを介して行われます。" 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`" msgid "Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`"
msgstr "一般的な :exc:`.BadArgument` の代わりに :exc:`.UserNotFound` を発生させます。" 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." 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 からユーザーを取得するようになりました。" msgstr "このコンバータは、ID が渡され、キャッシュされていない場合、HTTP API からユーザーを取得するようになりました。"
@ -4958,6 +5000,14 @@ msgstr "名前で検索"
msgid "Converts to a :class:`~discord.TextChannel`." msgid "Converts to a :class:`~discord.TextChannel`."
msgstr ":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.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.VoiceChannelConverter:12
#: ../../../discord/ext/commands/converter.py:docstring of discord.ext.commands.converter.CategoryChannelConverter: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." msgid "``Range[int, 1, 10]`` means the minimum is 1 and the maximum is 10."
msgstr "``Range[int, 1, 10]`` は最小値1、最大値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 #: ../../../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`." msgid "Inside a :class:`HybridCommand` this functions equivalently to :class:`discord.app_commands.Range`."
msgstr ":class:`HybridCommand` 内では、この関数は :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." 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` が適切なエラーハンドラに送出されます。" msgstr "もし値が渡された型に変換できず、または指定された範囲外である場合、:class:`~.ext.commands.BadArgument` や :class:`~.ext.commands.RangeError` が適切なエラーハンドラに送出されます。"
@ -5412,6 +5470,11 @@ msgstr "このパラメータの説明。"
msgid "The displayed default in :class:`Command.signature`." msgid "The displayed default in :class:`Command.signature`."
msgstr ":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 #: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.Parameter.get_default:3
msgid "Gets this parameter's default value." msgid "Gets this parameter's default value."
msgstr "このパラメータの既定値を取得します。" msgstr "このパラメータの既定値を取得します。"
@ -5441,8 +5504,8 @@ msgid "The displayed default in :attr:`Command.signature`."
msgstr ":attr:`Command.signature` で表示される既定値。" msgstr ":attr:`Command.signature` で表示される既定値。"
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:1 #: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:1
msgid "param(\\*, converter=..., default=..., description=..., displayed_default=...)" msgid "param(\\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...)"
msgstr "param(\\*, converter=..., default=..., description=..., displayed_default=...)" msgstr ""
#: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:3 #: ../../../discord/ext/commands/parameters.py:docstring of discord.ext.commands.parameters.parameter:3
msgid "An alias for :func:`parameter`." msgid "An alias for :func:`parameter`."
@ -5628,6 +5691,10 @@ msgstr "比較が失敗した値の、失敗した順のタプル。"
msgid "Tuple[Any, ``...``]" msgid "Tuple[Any, ``...``]"
msgstr "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 #: ../../../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." msgid "Exception raised when an operation does not work outside of private message contexts."
msgstr "プライベートメッセージコンテキスト外で、要求された処理が実行できない場合に発生する例外。" msgstr "プライベートメッセージコンテキスト外で、要求された処理が実行できない場合に発生する例外。"

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

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

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

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\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``:" msgid "If you want a more robust error system, you can derive from the exception and raise it instead of returning ``False``:"
msgstr "もし強化されたエラーシステムが必要な場合は、例外を継承し、``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`." 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` )。" msgstr "``guild_only`` デコレータはよく使われるため、標準で実装されています( :func:`~ext.commands.guild_only` )。"
#: ../../ext/commands/commands.rst:1164 #: ../../ext/commands/commands.rst:1165
msgid "Global Checks" msgid "Global Checks"
msgstr "グローバルチェック" 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." 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 "**すべての** コマンドにチェックをかけたいこともあるでしょう。そうしたい場合は、ライブラリのグローバルチェックを使うことができます。" 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." msgid "Global checks work similarly to regular checks except they are registered with the :meth:`.Bot.check` decorator."
msgstr "グローバルチェックは、 :meth:`.Bot.check` デコレータで登録されることを除き、通常のチェックと同様に動作します。" 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:" msgid "For example, to block all DMs we could do the following:"
msgstr "例えば、全DMをブロックするには、次の操作を行います。" 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." msgid "Be careful on how you write your global checks, as it could also lock you out of your own bot."
msgstr "グローバルチェックを追加するときには注意して下さい。ボットを操作できなくなる可能性があります。" msgstr "グローバルチェックを追加するときには注意して下さい。ボットを操作できなくなる可能性があります。"
#: ../../ext/commands/commands.rst:1187 #: ../../ext/commands/commands.rst:1188
msgid "Hybrid Commands" msgid "Hybrid Commands"
msgstr "ハイブリッドコマンド" 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." 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` は、テキストコマンドとしても、スラッシュコマンドとしても呼び出せるコマンドです。これを使用すれば、別々のコードを書かずにコマンドをスラッシュコマンドとテキストコマンドの両方として定義できます。" 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." 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` デコレータで装飾しないといけません。" 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." 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` を手動で同期しないといけません。" 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." msgid "You can create hybrid command groups and sub-commands using the :meth:`.Bot.hybrid_group` decorator."
msgstr ":meth:`.Bot.hybrid_group` デコレータを使用して、ハイブリッドコマンドグループとサブコマンドを作成できます。" 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." 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`` パラメータを使用して、親グループのコールバックを呼び出すサブコマンドを作成できます。" 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." 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 "スラッシュコマンドには制限があるため、ハイブリッドコマンドではテキストコマンドの一部の機能がサポートされていません。スラッシュコマンドでサポートされている機能のみ使用している場合にハイブリッドコマンドを定義できます。" msgstr "スラッシュコマンドには制限があるため、ハイブリッドコマンドではテキストコマンドの一部の機能がサポートされていません。スラッシュコマンドでサポートされている機能のみ使用している場合にハイブリッドコマンドを定義できます。"
#: ../../ext/commands/commands.rst:1235 #: ../../ext/commands/commands.rst:1236
msgid "Following are currently **not supported** by hybrid commands:" msgid "Following are currently **not supported** by hybrid commands:"
msgstr "以下は現時点でハイブリッドコマンドではサポート **されていません**:" msgstr "以下は現時点でハイブリッドコマンドではサポート **されていません**:"
#: ../../ext/commands/commands.rst:1237 #: ../../ext/commands/commands.rst:1238
msgid "Variable number of arguments. e.g. ``*arg: int``" msgid "Variable number of arguments. e.g. ``*arg: int``"
msgstr "可変長引数。例: ``*arg: int``" msgstr "可変長引数。例: ``*arg: int``"
#: ../../ext/commands/commands.rst:1238 #: ../../ext/commands/commands.rst:1239
msgid "Group commands with a depth greater than 1." msgid "Group commands with a depth greater than 1."
msgstr "深さが1より大きいグループコマンド。" msgstr "深さが1より大きいグループコマンド。"
#: ../../ext/commands/commands.rst:1242 #: ../../ext/commands/commands.rst:1243
msgid "Most :class:`typing.Union` types." msgid "Most :class:`typing.Union` types."
msgstr "ほとんどの :class:`typing.Union` 型。" msgstr "ほとんどの :class:`typing.Union` 型。"
#: ../../ext/commands/commands.rst:1240 #: ../../ext/commands/commands.rst:1241
msgid "Unions of channel types are allowed" msgid "Unions of channel types are allowed"
msgstr "チャンネルの型のユニオン型は使用できます" msgstr "チャンネルの型のユニオン型は使用できます"
#: ../../ext/commands/commands.rst:1241 #: ../../ext/commands/commands.rst:1242
msgid "Unions of user types are allowed" msgid "Unions of user types are allowed"
msgstr "ユーザーの型のユニオン型は使用できます" msgstr "ユーザーの型のユニオン型は使用できます"
#: ../../ext/commands/commands.rst:1242 #: ../../ext/commands/commands.rst:1243
msgid "Unions of user types with roles are allowed" msgid "Unions of user types with roles are allowed"
msgstr "チャンネルの型とロールの型のユニオン型は使用できます" 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." 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` デコレータの下に配置しないといけません。" 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:" 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` クラスのメソッドや属性の動作が変化します:" 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." msgid ":attr:`.Context.interaction` can be used to retrieve the slash command interaction."
msgstr ":attr:`.Context.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." 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` は、インタラクション応答とフォローアップ応答のどちらを送信するかを自動的に決定します。" 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." msgid ":meth:`.Context.defer` defers the interaction response for slash commands but shows typing indicator for text commands."
msgstr ":meth:`.Context.defer` はスラッシュコマンドではインタラクション応答を遅らせ、テキストコマンドでは入力インジケーターを表示します。" msgstr ":meth:`.Context.defer` はスラッシュコマンドではインタラクション応答を遅らせ、テキストコマンドでは入力インジケーターを表示します。"

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

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\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`` というエントリポイントが用意されています。" msgstr "稀ではありますが、エクステンションにクリーンアップが必要だったり、いつアンロードするかを確認したい場合があります。このために ``setup`` に似たエクステンションがアンロードされるときに呼び出される ``teardown`` というエントリポイントが用意されています。"
#: ../../ext/commands/extensions.rst:57 #: ../../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" msgid "basic_ext.py"
msgstr "basic_ext.py" msgstr "basic_ext.py"

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

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

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

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

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

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

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

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

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

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

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

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-29 20:45+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -62,7 +62,7 @@ msgid "type"
msgstr "型" msgstr "型"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:12 #: ../../../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/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:36
#: ../../../discord/components.py:docstring of discord.components.SelectMenu:43 #: ../../../discord/components.py:docstring of discord.components.SelectMenu:43
@ -84,7 +84,7 @@ msgid "The guild ID the interaction was sent from."
msgstr "インタラクションが送信されたギルドのID。" msgstr "インタラクションが送信されたギルドのID。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:24 #: ../../../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:49
#: ../../../discord/components.py:docstring of discord.components.TextInput:55 #: ../../../discord/components.py:docstring of discord.components.TextInput:55
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommand:91 #: ../../../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`]" msgstr "Optional[:class:`int`]"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:28 #: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:28
msgid "The channel ID the interaction was sent from." msgid "The channel the interaction was sent from."
msgstr "インタラクションが送信されたチャンネルのID。" 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." msgid "The application ID that the interaction was for."
msgstr "インタラクションの対象となったアプリケーションのID。" 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." msgid "The user or member that sent the interaction."
msgstr "インタラクションを送信したユーザーまたはメンバー。" 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 #: ../../../discord/message.py:docstring of discord.message.MessageInteraction:41
msgid "Union[:class:`User`, :class:`Member`]" msgid "Union[:class:`User`, :class:`Member`]"
msgstr "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." msgid "The message that sent this interaction."
msgstr "このインタラクションを送信したメッセージ。" 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." msgid "This is only available for :attr:`InteractionType.component` interactions."
msgstr "これは :attr:`InteractionType.component` インタラクションの場合にのみ使用できます。" 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`]" msgid "Optional[:class:`Message`]"
msgstr "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." msgid "The token to continue the interaction. These are valid for 15 minutes."
msgstr "インタラクションを続行するのに使うトークン。有効期限は15分です。" 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 #: ../../docstring of discord.InteractionMessage.clean_content:15
#: ../../../discord/interactions.py:docstring of discord.InteractionMessage.jump_url:3 #: ../../../discord/interactions.py:docstring of discord.InteractionMessage.jump_url:3
#: ../../docstring of discord.InteractionMessage.system_content:8 #: ../../docstring of discord.InteractionMessage.system_content:8
@ -132,43 +140,43 @@ msgstr "インタラクションを続行するのに使うトークン。有効
msgid ":class:`str`" msgid ":class:`str`"
msgstr ":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." msgid "The raw interaction data."
msgstr "生のインタラクションデータ。" msgstr "生のインタラクションデータ。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:63 #: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:65
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:83 #: ../../../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.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.ContextMenu:80
#: ../../../discord/app_commands/commands.py:docstring of discord.app_commands.commands.Group:105 #: ../../../discord/app_commands/commands.py:docstring of discord.app_commands.commands.Group:105
msgid ":class:`dict`" msgid ":class:`dict`"
msgstr ":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." msgid "The locale of the user invoking the interaction."
msgstr "インタラクションを呼び出したユーザーのロケール。" msgstr "インタラクションを呼び出したユーザーのロケール。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:69 #: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:71
msgid ":class:`Locale`" msgid ":class:`Locale`"
msgstr ":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." msgid "The preferred locale of the guild the interaction was sent from, if any."
msgstr "インタラクションの送信元のギルドの優先ロケール。もし無ければ ``None`` となります。" msgstr "インタラクションの送信元のギルドの優先ロケール。もし無ければ ``None`` となります。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:75 #: ../../../discord/interactions.py:docstring of discord.interactions.Interaction:77
msgid "Optional[:class:`Locale`]" msgid "Optional[:class:`Locale`]"
msgstr "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." 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 "インタラクションの処理中に使用する追加のデータを保管できる辞書型。ライブラリは辞書型の中のキーや値を一切操作しません。" 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." msgid "Whether the command associated with this interaction failed to execute. This includes checks and execution."
msgstr "このインタラクションに関連付けられたコマンドの実行に失敗したかどうか。これにはチェックとコマンドの実行が含まれます。" 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.Button:35
#: ../../../discord/components.py:docstring of discord.components.SelectMenu:55 #: ../../../discord/components.py:docstring of discord.components.SelectMenu:55
#: ../../../discord/components.py:docstring of discord.components.TextInput:43 #: ../../../discord/components.py:docstring of discord.components.TextInput:43
@ -196,17 +204,9 @@ msgstr "インタラクションが送信されたギルド。"
msgid "Optional[:class:`Guild`]" msgid "Optional[:class:`Guild`]"
msgstr "Optional[:class:`Guild`]" msgstr "Optional[:class:`Guild`]"
#: ../../docstring of discord.Interaction.channel:1 #: ../../../discord/interactions.py:docstring of discord.Interaction.channel_id:1
msgid "The channel the interaction was sent from." msgid "The ID of the channel the interaction was sent from."
msgstr "インタラクションが送信されたチャンネル。" 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.permissions:1 #: ../../../discord/interactions.py:docstring of discord.Interaction.permissions:1
msgid "The resolved permissions of the member in the channel, including overwrites." 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`` の両方を指定した場合。" msgstr "``embed`` と ``embeds`` の両方を指定した場合。"
#: ../../../discord/interactions.py:docstring of discord.interactions.Interaction.edit_original_response:36 #: ../../../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 #: ../../../discord/interactions.py:docstring of discord.interactions.InteractionMessage.edit:35
msgid "The length of ``embeds`` was invalid." msgid "The length of ``embeds`` was invalid."
msgstr "``embeds`` の長さが無効だった場合。" 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.defer:25
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.pong:8 #: ../../../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.edit_message:35
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_modal:9 #: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_modal:9
msgid "This interaction has already been responded to before." 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`` に設定されている場合、埋め込みなしでメッセージを送信します。" msgstr "メッセージの埋め込みを抑制するかどうか。これが ``True`` に設定されている場合、埋め込みなしでメッセージを送信します。"
#: ../../../discord/interactions.py:docstring of discord.interactions.InteractionResponse.send_message:30 #: ../../../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 #: ../../../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." 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 "指定すると、これはメッセージを送信したあと削除するまでにバックグラウンドで待機する秒数となります。もし削除が失敗しても、それは静かに無視されます。" 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 #: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.reply:12
msgid "Sending the message failed." msgid "Sending the message failed."
msgstr "メッセージの送信に失敗した場合。" 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``." msgid "You specified both ``embed`` and ``embeds`` or ``file`` and ``files``."
msgstr "``embed`` と ``embeds`` または ``file`` と ``files`` の両方を指定した場合。" msgstr "``embed`` と ``embeds`` または ``file`` と ``files`` の両方を指定した場合。"
@ -851,34 +855,42 @@ msgid "The name of the thread."
msgstr "スレッドの名前。" msgstr "スレッドの名前。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.create_thread:14 #: ../../../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." 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 "スレッドが非アクティブ時に、自動的にアーカイブされるまでの分単位の長さ。指定しない場合は、チャンネルのデフォルトの自動アーカイブ期間が使用されます。" 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 #: ../../../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``." 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`` でこの場合は低速モードレート制限が無しとなります。" 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." msgid "The reason for creating a new thread. Shows up on the audit log."
msgstr "スレッドを作成する理由。監査ログに表示されます。" 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." msgid "You do not have permissions to create a thread."
msgstr "スレッドを作成する権限を持っていない場合。" 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." msgid "Creating the thread failed."
msgstr "スレッドの作成に失敗した場合。" 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." msgid "This message does not have guild info attached."
msgstr "メッセージがギルド情報を持っていない場合。" 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." msgid "The created thread."
msgstr "作成されたスレッド" 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`" msgid ":class:`.Thread`"
msgstr ":class:`Thread`" msgstr ":class:`Thread`"
@ -957,22 +969,22 @@ msgid "Pinning the message failed, probably due to the channel having more t
msgstr "チャンネルにすでに50個ピン留めされたメッセージがあるなどの理由で、メッセージのピン留めに失敗した場合。" msgstr "チャンネルにすでに50個ピン留めされたメッセージがあるなどの理由で、メッセージのピン留めに失敗した場合。"
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:3 #: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:3
msgid "Publishes this message to your announcement channel." msgid "Publishes this message to the channel's followers."
msgstr "このメッセージをアナウンスチャンネルに公開します。" msgstr ""
#: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:5 #: ../../../discord/interactions.py:docstring of discord.message.PartialMessage.publish:5
msgid "You must have :attr:`~Permissions.send_messages` to do this." msgid "The message must have been sent in a news channel. You must have :attr:`~Permissions.send_messages` to do this."
msgstr "これを行うには、 :attr:`~Permissions.send_messages` が必要です。" 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." msgid "If the message is not your own then :attr:`~Permissions.manage_messages` is also needed."
msgstr "自身のメッセージ以外の場合は :attr:`~Permissions.manage_messages` も必要です。" 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 #: ../../../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." msgid "Publishing the message failed."
msgstr "メッセージの公開に失敗した場合。" msgstr "メッセージの公開に失敗した場合。"
@ -1843,8 +1855,8 @@ msgid "The user's ID that archived this thread."
msgstr "このスレッドをアーカイブしたユーザーのID。" msgstr "このスレッドをアーカイブしたユーザーのID。"
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommandThread:87 #: ../../../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." 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 "非アクティブのスレッドが自動的にアーカイブされるまでの分数です。通常は60、1440、4320、10080の値を設定します。" msgstr ""
#: ../../../discord/app_commands/models.py:docstring of discord.app_commands.models.AppCommandThread:94 #: ../../../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." 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を作成するには、このオブジェクトを継承する必要があります。" msgstr "Discord内でUIを作成するには、このオブジェクトを継承する必要があります。"
#: ../../../discord/ui/view.py:docstring of discord.ui.view.View:7 #: ../../../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." 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`` の場合タイムアウトはありません。" msgstr "UIの最後のインタラクションから起算した、入力を受け付けなくなるまでの秒単位のタイムアウト。 ``None`` の場合タイムアウトはありません。"
@ -2677,19 +2689,19 @@ msgstr "Discord内でモーダルポップアップウィンドウを作成す
msgid "Examples" msgid "Examples"
msgstr "例" 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." msgid "The title of the modal. Can only be up to 45 characters."
msgstr "モーダルのタイトル。最大45文字までです。" 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." 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 文字までしか使用できません。" 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." msgid "The title of the modal."
msgstr "モーダルのタイトル。" 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." msgid "The ID of the modal that gets received during an interaction."
msgstr "インタラクション中に受け取るモーダルID。" msgstr "インタラクション中に受け取るモーダルID。"

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

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -109,15 +109,19 @@ msgstr "いつものようにpipインストールを実行します。"
msgid "Congratulations. You now have a virtual environment all set up." msgid "Congratulations. You now have a virtual environment all set up."
msgstr "おめでとうございます。これで仮想環境のセットアップができました。" 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" msgid "Basic Concepts"
msgstr "基本概念" 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." 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>` の概念を中心としています。イベントは何かを受け取り、それに対する応答を行います。例えば、メッセージが発生すると、メッセージの発生に関連するイベントを受け取り、そのイベントに対して応答を返すことができます。" msgstr "discord.pyは :ref:`イベント <discord-api-events>` の概念を中心としています。イベントは何かを受け取り、それに対する応答を行います。例えば、メッセージが発生すると、メッセージの発生に関連するイベントを受け取り、そのイベントに対して応答を返すことができます。"
#: ../../intro.rst:98 #: ../../intro.rst:103
msgid "A quick example to showcase how events work:" msgid "A quick example to showcase how events work:"
msgstr "以下はイベントの仕組みを紹介する簡単な例です。" msgstr "以下はイベントの仕組みを紹介する簡単な例です。"

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

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

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

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

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

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

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

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

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

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: discordpy\n" "Project-Id-Version: discordpy\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-24 11:01+0000\n" "POT-Creation-Date: 2023-06-21 01:17+0000\n"
"PO-Revision-Date: 2023-01-30 13:38\n" "PO-Revision-Date: 2023-06-21 01:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"MIME-Version: 1.0\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. 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`: 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 .. 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 This page keeps a detailed human friendly rendering of what's new and changed
in specific versions. 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: .. _vp2p3p1:
v2.3.1 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. # This example covers advanced startup options and uses some real world examples for why you may need them.
import asyncio import asyncio
@ -88,9 +90,16 @@ async def main():
# 2. We become responsible for starting the bot. # 2. We become responsible for starting the bot.
exts = ['general', 'mod', 'dice'] exts = ['general', 'mod', 'dice']
async with CustomBot(commands.when_mentioned, db_pool=pool, web_client=our_client, initial_extensions=exts) as bot: intents = discord.Intents.default()
intents.message_content = True
await bot.start(os.getenv('TOKEN', '')) 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: # 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. # This example requires the 'message_content' privileged intent to function.
from __future__ import annotations
from discord.ext import commands from discord.ext import commands
import discord import discord
import re
# Define a simple View that persists between bot restarts # 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) 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): class PersistentViewBot(commands.Bot):
def __init__(self): def __init__(self):
intents = discord.Intents.default() 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 # If you have the message_id you can also pass it as a keyword argument, but for this example
# we don't have one. # we don't have one.
self.add_view(PersistentView()) 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): async def on_ready(self):
print(f'Logged in as {self.user} (ID: {self.user.id})') 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()) 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') bot.run('token')

1
requirements.txt

@ -1 +1,2 @@
aiohttp>=3.7.4,<4 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_trio==1.1.2',
'sphinxcontrib-websupport', 'sphinxcontrib-websupport',
'typing-extensions>=4.3,<5', 'typing-extensions>=4.3,<5',
'sphinx-inline-tabs',
], ],
'speed': [ 'speed': [
'orjson>=3.5.4', 'orjson>=3.5.4',
@ -53,6 +54,7 @@ extras_require = {
'pytest-cov', 'pytest-cov',
'pytest-mock', 'pytest-mock',
'typing-extensions>=4.3,<5', 'typing-extensions>=4.3,<5',
'tzdata; sys_platform == "win32"',
], ],
} }
@ -93,6 +95,7 @@ setup(
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules', '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.inner.my_command.parent is cog.inner
assert cog.my_inner_command.parent is cog.inner assert cog.my_inner_command.parent is cog.inner
assert cog.my_inner_command.binding is cog 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