diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b233a5deff09..79b7ac8ec766 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,7 +38,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: - version: '1.1.316' + version: '1.1.394' warnings: false no-comments: ${{ matrix.python-version != '3.x' }} diff --git a/README.rst b/README.rst index 621a69500291..b2112f9d6b81 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,13 @@ Installing To install the library without full voice support, you can just run the following command: +.. note:: + + A `Virtual Environment `__ is recommended to install + the library, especially on Linux where the system Python is externally managed and restricts which + packages you can install on it. + + .. code:: sh # Linux/macOS diff --git a/discord/__init__.py b/discord/__init__.py index d239c8f3b383..48fe1092541e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.4.0a' +__version__ = '2.6.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -69,6 +69,10 @@ from .components import * from .threads import * from .automod import * +from .poll import * +from .soundboard import * +from .subscription import * +from .presences import * class VersionInfo(NamedTuple): @@ -79,8 +83,14 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) +# This is a backwards compatibility hack and should be removed in v3 +# Essentially forcing the exception to have different base classes +# In the future, this should only inherit from ClientException +if len(MissingApplicationID.__bases__) == 1: + MissingApplicationID.__bases__ = (app_commands.AppCommandError, ClientException) + del logging, NamedTuple, Literal, VersionInfo diff --git a/discord/__main__.py b/discord/__main__.py index 843274b53a76..f8556fcdc1ae 100644 --- a/discord/__main__.py +++ b/discord/__main__.py @@ -28,7 +28,7 @@ import argparse import sys -from pathlib import Path +from pathlib import Path, PurePath, PureWindowsPath import discord import importlib.metadata @@ -225,8 +225,14 @@ def to_path(parser: argparse.ArgumentParser, name: str, *, replace_spaces: bool ) if len(name) <= 4 and name.upper() in forbidden: parser.error('invalid directory name given, use a different one') + path = PurePath(name) + if isinstance(path, PureWindowsPath) and path.drive: + drive, rest = path.parts[0], path.parts[1:] + transformed = tuple(map(lambda p: p.translate(_translation_table), rest)) + name = drive + '\\'.join(transformed) - name = name.translate(_translation_table) + else: + name = name.translate(_translation_table) if replace_spaces: name = name.replace(' ', '-') return Path(name) diff --git a/discord/abc.py b/discord/abc.py index 64a0b0041210..5ea20b558a70 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -26,6 +26,7 @@ import copy import time +import secrets import asyncio from datetime import datetime from typing import ( @@ -49,7 +50,7 @@ from .object import OLDEST_OBJECT, Object from .context_managers import Typing from .enums import ChannelType, InviteTarget -from .errors import ClientException +from .errors import ClientException, NotFound from .mentions import AllowedMentions from .permissions import PermissionOverwrite, Permissions from .role import Role @@ -92,14 +93,18 @@ VoiceChannel, StageChannel, ) + from .poll import Poll from .threads import Thread - from .ui.view import View + from .ui.view import BaseView, View, LayoutView from .types.channel import ( PermissionOverwrite as PermissionOverwritePayload, Channel as ChannelPayload, GuildChannel as GuildChannelPayload, OverwriteType, ) + from .types.guild import ( + ChannelPositionUpdate, + ) from .types.snowflake import ( SnowflakeList, ) @@ -121,7 +126,14 @@ def __repr__(self) -> str: async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): for m in messages: - await m.delete() + try: + await m.delete() + except NotFound as exc: + if exc.code == 10008: + continue # bulk deletion ignores not found messages, single deletion does not. + # several other race conditions with deletion should fail without continuing, + # such as the channel being deleted and not found. + raise async def _purge_helper( @@ -248,6 +260,22 @@ def avatar(self) -> Optional[Asset]: """Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar, if present.""" raise NotImplementedError + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar decoration, if present. + + .. versionadded:: 2.4 + """ + raise NotImplementedError + + @property + def avatar_decoration_sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns an integer that represents the user's avatar decoration SKU ID, if present. + + .. versionadded:: 2.4 + """ + raise NotImplementedError + @property def default_avatar(self) -> Asset: """:class:`~discord.Asset`: Returns the default avatar for a given user.""" @@ -682,6 +710,7 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: - Member overrides - Implicit permissions - Member timeout + - User installed app If a :class:`~discord.Role` is passed, then it checks the permissions someone with that role would have, which is essentially: @@ -697,6 +726,12 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: .. versionchanged:: 2.0 ``obj`` parameter is now positional-only. + .. versionchanged:: 2.4 + User installed apps are now taken into account. + The permissions returned for a user installed app mirrors the + permissions Discord returns in :attr:`~discord.Interaction.app_permissions`, + though it is recommended to use that attribute instead. + Parameters ---------- obj: Union[:class:`~discord.Member`, :class:`~discord.Role`] @@ -728,6 +763,13 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: return Permissions.all() default = self.guild.default_role + if default is None: + + if self._state.self_id == obj.id: + return Permissions._user_installed_permissions(in_guild=True) + else: + return Permissions.none() + base = Permissions(default.permissions.value) # Handle the role case first @@ -967,11 +1009,15 @@ async def _clone_impl( base_attrs: Dict[str, Any], *, name: Optional[str] = None, + category: Optional[CategoryChannel] = None, reason: Optional[str] = None, ) -> Self: base_attrs['permission_overwrites'] = [x._asdict() for x in self._overwrites] base_attrs['parent_id'] = self.category_id base_attrs['name'] = name or self.name + if category is not None: + base_attrs['parent_id'] = category.id + guild_id = self.guild.id cls = self.__class__ data = await self._state.http.create_channel(guild_id, self.type.value, reason=reason, **base_attrs) @@ -981,7 +1027,13 @@ async def _clone_impl( self.guild._channels[obj.id] = obj # type: ignore # obj is a GuildChannel return obj - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> Self: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> Self: """|coro| Clones this channel. This creates a channel with the same properties @@ -996,6 +1048,11 @@ async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = Non name: Optional[:class:`str`] The name of the new channel. If not provided, defaults to this channel name. + category: Optional[:class:`~discord.CategoryChannel`] + The category the new channel belongs to. + This parameter is ignored if cloning a category channel. + + .. versionadded:: 2.5 reason: Optional[:class:`str`] The reason for cloning this channel. Shows up on the audit log. @@ -1092,10 +1149,10 @@ async def move(self, **kwargs: Any) -> None: channel list (or category if given). This is mutually exclusive with ``beginning``, ``before``, and ``after``. before: :class:`~discord.abc.Snowflake` - The channel that should be before our current channel. + Whether to move the channel before the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``after``. after: :class:`~discord.abc.Snowflake` - The channel that should be after our current channel. + Whether to move the channel after the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``before``. offset: :class:`int` The number of channels to offset the move by. For example, @@ -1178,11 +1235,11 @@ async def move(self, **kwargs: Any) -> None: raise ValueError('Could not resolve appropriate move position') channels.insert(max((index + offset), 0), self) - payload = [] + payload: List[ChannelPositionUpdate] = [] lock_permissions = kwargs.get('sync_permissions', False) reason = kwargs.get('reason') for index, channel in enumerate(channels): - d = {'id': channel.id, 'position': index} + d: ChannelPositionUpdate = {'id': channel.id, 'position': index} if parent_id is not MISSING and channel.id == self.id: d.update(parent_id=parent_id, lock_permissions=lock_permissions) payload.append(d) @@ -1317,6 +1374,38 @@ class Messageable: async def _get_channel(self) -> MessageableChannel: raise NotImplementedError + @overload + async def send( + self, + *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def send( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def send( self, @@ -1334,6 +1423,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1354,6 +1444,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1374,6 +1465,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1394,6 +1486,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1412,9 +1505,10 @@ async def send( allowed_mentions: Optional[AllowedMentions] = None, reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, - view: Optional[View] = None, + view: Optional[BaseView] = None, suppress_embeds: bool = False, silent: bool = False, + poll: Optional[Poll] = None, ) -> Message: """|coro| @@ -1472,10 +1566,11 @@ async def send( .. versionadded:: 1.4 reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`, :class:`~discord.PartialMessage`] - A reference to the :class:`~discord.Message` to which you are replying, this can be created using - :meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. You can control - whether this mentions the author of the referenced message using the :attr:`~discord.AllowedMentions.replied_user` - attribute of ``allowed_mentions`` or by setting ``mention_author``. + A reference to the :class:`~discord.Message` to which you are referencing, this can be created using + :meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. + In the event of a replying reference, you can control whether this mentions the author of the referenced + message using the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions`` or by + setting ``mention_author``. .. versionadded:: 1.6 @@ -1483,10 +1578,12 @@ async def send( If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. .. versionadded:: 1.6 - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. @@ -1500,6 +1597,10 @@ async def send( in the UI, but will not actually send a notification. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 Raises -------- @@ -1507,6 +1608,9 @@ async def send( Sending the message failed. ~discord.Forbidden You do not have the proper permissions to send the message. + ~discord.NotFound + You sent a message with the same nonce as one that has been explicitly + deleted shortly earlier. ValueError The ``files`` or ``embeds`` list is not of the appropriate size. TypeError @@ -1551,6 +1655,9 @@ async def send( else: flags = MISSING + if nonce is None: + nonce = secrets.randbits(64) + with handle_message_parameters( content=content, tts=tts, @@ -1566,6 +1673,7 @@ async def send( stickers=sticker_ids, view=view, flags=flags, + poll=poll, ) as params: data = await state.http.send_message(channel.id, params=params) @@ -1573,6 +1681,9 @@ async def send( if view and not view.is_finished(): state.store_view(view, ret.id) + if poll: + poll._update(ret) + if delete_after is not None: await ret.delete(delay=delete_after) return ret diff --git a/discord/activity.py b/discord/activity.py index c692443f987a..324bea42f290 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -273,7 +273,7 @@ def to_dict(self) -> Dict[str, Any]: def start(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.""" try: - timestamp = self.timestamps['start'] / 1000 + timestamp = self.timestamps['start'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -283,7 +283,7 @@ def start(self) -> Optional[datetime.datetime]: def end(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.""" try: - timestamp = self.timestamps['end'] / 1000 + timestamp = self.timestamps['end'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -293,7 +293,7 @@ def end(self) -> Optional[datetime.datetime]: def large_image_url(self) -> Optional[str]: """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.""" try: - large_image = self.assets['large_image'] + large_image = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -303,7 +303,7 @@ def large_image_url(self) -> Optional[str]: def small_image_url(self) -> Optional[str]: """Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable.""" try: - small_image = self.assets['small_image'] + small_image = self.assets['small_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -525,7 +525,7 @@ def twitch_name(self) -> Optional[str]: """ try: - name = self.assets['large_image'] + name = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: diff --git a/discord/app_commands/__init__.py b/discord/app_commands/__init__.py index 971461713449..a338cab75dc5 100644 --- a/discord/app_commands/__init__.py +++ b/discord/app_commands/__init__.py @@ -16,5 +16,6 @@ from .namespace import * from .transformers import * from .translator import * +from .installs import * from . import checks as checks from .checks import Cooldown as Cooldown diff --git a/discord/app_commands/checks.py b/discord/app_commands/checks.py index f6c09481d549..5c17b951c294 100644 --- a/discord/app_commands/checks.py +++ b/discord/app_commands/checks.py @@ -186,7 +186,7 @@ def copy(self) -> Self: :class:`Cooldown` A new instance of this cooldown. """ - return Cooldown(self.rate, self.per) + return self.__class__(self.rate, self.per) def __repr__(self) -> str: return f'' diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 6c2aae7b8df6..d5b8d93b27e5 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -49,6 +49,7 @@ from copy import copy as shallow_copy from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale +from .installs import AppCommandContext, AppInstallationType from .models import Choice from .transformers import annotation_to_parameter, CommandParameter, NoneType from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered @@ -65,6 +66,8 @@ from ..abc import Snowflake from .namespace import Namespace from .models import ChoiceT + from .tree import CommandTree + from .._types import ClientT # Generally, these two libraries are supposed to be separate from each other. # However, for type hinting purposes it's unfortunately necessary for one to @@ -87,6 +90,12 @@ 'autocomplete', 'guilds', 'guild_only', + 'dm_only', + 'private_channel_only', + 'allowed_contexts', + 'guild_install', + 'user_install', + 'allowed_installs', 'default_permissions', ) @@ -618,6 +627,16 @@ class Command(Generic[GroupT, P, T]): Whether the command should only be usable in guild contexts. Due to a Discord limitation, this does not work on subcommands. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that the command is allowed to be used in. + Overrides ``guild_only`` if this is set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. @@ -638,6 +657,8 @@ def __init__( nsfw: bool = False, parent: Optional[Group] = None, guild_ids: Optional[List[int]] = None, + allowed_contexts: Optional[AppCommandContext] = None, + allowed_installs: Optional[AppInstallationType] = None, auto_locale_strings: bool = True, extras: Dict[Any, Any] = MISSING, ): @@ -672,6 +693,13 @@ def __init__( callback, '__discord_app_commands_default_permissions__', None ) self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False) + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr( + callback, '__discord_app_commands_contexts__', None + ) + self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr( + callback, '__discord_app_commands_installation_types__', None + ) + self.nsfw: bool = nsfw self.extras: Dict[Any, Any] = extras or {} @@ -718,8 +746,8 @@ def _copy_with( return copy - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} @@ -745,7 +773,7 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] ] return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: # If we have a parent then our type is a subcommand # Otherwise, the type falls back to the specific command type (e.g. slash command or context menu) option_type = AppCommandType.chat_input.value if self.parent is None else AppCommandOptionType.subcommand.value @@ -760,6 +788,8 @@ def to_dict(self) -> Dict[str, Any]: base['nsfw'] = self.nsfw base['dm_permission'] = not self.guild_only base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value + base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts) + base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs) return base @@ -873,7 +903,7 @@ async def _invoke_autocomplete(self, interaction: Interaction, name: str, namesp predicates = getattr(param.autocomplete, '__discord_app_commands_checks__', []) if predicates: try: - passed = await async_all(f(interaction) for f in predicates) + passed = await async_all(f(interaction) for f in predicates) # type: ignore except Exception: passed = False @@ -984,7 +1014,7 @@ async def _check_can_run(self, interaction: Interaction) -> bool: if not predicates: return True - return await async_all(f(interaction) for f in predicates) + return await async_all(f(interaction) for f in predicates) # type: ignore def error(self, coro: Error[GroupT]) -> Error[GroupT]: """A decorator that registers a coroutine as a local error handler. @@ -1068,7 +1098,7 @@ async def fruits_autocomplete( def decorator(coro: AutocompleteCallback[GroupT, ChoiceT]) -> AutocompleteCallback[GroupT, ChoiceT]: if not inspect.iscoroutinefunction(coro): - raise TypeError('The error handler must be a coroutine.') + raise TypeError('The autocomplete callback must be a coroutine function.') try: param = self._params[name] @@ -1167,6 +1197,16 @@ class ContextMenu: guild_only: :class:`bool` Whether the command should only be usable in guild contexts. Defaults to ``False``. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this context menu is allowed to be used in. + Overrides ``guild_only`` if set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``. @@ -1189,6 +1229,8 @@ def __init__( type: AppCommandType = MISSING, nsfw: bool = False, guild_ids: Optional[List[int]] = None, + allowed_contexts: Optional[AppCommandContext] = None, + allowed_installs: Optional[AppInstallationType] = None, auto_locale_strings: bool = True, extras: Dict[Any, Any] = MISSING, ): @@ -1214,6 +1256,12 @@ def __init__( ) self.nsfw: bool = nsfw self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False) + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr( + callback, '__discord_app_commands_contexts__', None + ) + self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr( + callback, '__discord_app_commands_installation_types__', None + ) self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) self.extras: Dict[Any, Any] = extras or {} @@ -1231,8 +1279,8 @@ def qualified_name(self) -> str: """:class:`str`: Returns the fully qualified command name.""" return self.name - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) context = TranslationContext(location=TranslationContextLocation.command_name, data=self) if self._locale_name: name_localizations: Dict[str, str] = {} @@ -1244,11 +1292,13 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] base['name_localizations'] = name_localizations return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: return { 'name': self.name, 'type': self.type.value, 'dm_permission': not self.guild_only, + 'contexts': tree.allowed_contexts._merge_to_array(self.allowed_contexts), + 'integration_types': tree.allowed_installs._merge_to_array(self.allowed_installs), 'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value, 'nsfw': self.nsfw, } @@ -1258,7 +1308,7 @@ async def _check_can_run(self, interaction: Interaction) -> bool: if not predicates: return True - return await async_all(f(interaction) for f in predicates) + return await async_all(f(interaction) for f in predicates) # type: ignore def _has_any_error_handlers(self) -> bool: return self.on_error is not None @@ -1405,6 +1455,16 @@ class shortened to 100 characters. Whether the group should only be usable in guild contexts. Due to a Discord limitation, this does not work on subcommands. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this group is allowed to be used in. Overrides + guild_only if set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. @@ -1424,6 +1484,8 @@ class shortened to 100 characters. __discord_app_commands_group_locale_description__: Optional[locale_str] = None __discord_app_commands_group_nsfw__: bool = False __discord_app_commands_guild_only__: bool = MISSING + __discord_app_commands_contexts__: Optional[AppCommandContext] = MISSING + __discord_app_commands_installation_types__: Optional[AppInstallationType] = MISSING __discord_app_commands_default_permissions__: Optional[Permissions] = MISSING __discord_app_commands_has_module__: bool = False __discord_app_commands_error_handler__: Optional[ @@ -1492,6 +1554,8 @@ def __init__( parent: Optional[Group] = None, guild_ids: Optional[List[int]] = None, guild_only: bool = MISSING, + allowed_contexts: Optional[AppCommandContext] = MISSING, + allowed_installs: Optional[AppInstallationType] = MISSING, nsfw: bool = MISSING, auto_locale_strings: bool = True, default_permissions: Optional[Permissions] = MISSING, @@ -1540,6 +1604,22 @@ def __init__( self.guild_only: bool = guild_only + if allowed_contexts is MISSING: + if cls.__discord_app_commands_contexts__ is MISSING: + allowed_contexts = None + else: + allowed_contexts = cls.__discord_app_commands_contexts__ + + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts + + if allowed_installs is MISSING: + if cls.__discord_app_commands_installation_types__ is MISSING: + allowed_installs = None + else: + allowed_installs = cls.__discord_app_commands_installation_types__ + + self.allowed_installs: Optional[AppInstallationType] = allowed_installs + if nsfw is MISSING: nsfw = cls.__discord_app_commands_group_nsfw__ @@ -1633,8 +1713,8 @@ def _copy_with( return copy - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} @@ -1654,10 +1734,10 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] base['name_localizations'] = name_localizations base['description_localizations'] = description_localizations - base['options'] = [await child.get_translated_payload(translator) for child in self._children.values()] + base['options'] = [await child.get_translated_payload(tree, translator) for child in self._children.values()] return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: # If this has a parent command then it's part of a subcommand group # Otherwise, it's just a regular command option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value @@ -1665,13 +1745,15 @@ def to_dict(self) -> Dict[str, Any]: 'name': self.name, 'description': self.description, 'type': option_type, - 'options': [child.to_dict() for child in self._children.values()], + 'options': [child.to_dict(tree) for child in self._children.values()], } if self.parent is None: base['nsfw'] = self.nsfw base['dm_permission'] = not self.guild_only base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value + base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts) + base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs) return base @@ -1760,7 +1842,7 @@ def error(self, coro: ErrorFunc) -> ErrorFunc: if len(params) != 2: raise TypeError('The error handler must have 2 parameters.') - self.on_error = coro + self.on_error = coro # type: ignore return coro async def interaction_check(self, interaction: Interaction, /) -> bool: @@ -2290,6 +2372,12 @@ def guilds(*guild_ids: Union[Snowflake, int]) -> Callable[[T], T]: with the :meth:`CommandTree.command` or :meth:`CommandTree.context_menu` decorator then this must go below that decorator. + .. note :: + + Due to a Discord limitation, this decorator cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + Example: .. code-block:: python3 @@ -2421,8 +2509,265 @@ async def my_guild_only_command(interaction: discord.Interaction) -> None: def inner(f: T) -> T: if isinstance(f, (Command, Group, ContextMenu)): f.guild_only = True + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts else: f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment + + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.guild = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +@overload +def private_channel_only(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def private_channel_only(func: T) -> T: + ... + + +def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command can only be used in the context of DMs and group DMs. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + Therefore, there is no error handler called when a command is used within a guild. + + This decorator can be called with or without parentheses. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + .. versionadded:: 2.4 + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.private_channel_only() + async def my_private_channel_only_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in DMs and GDMs!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.private_channel = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +@overload +def dm_only(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def dm_only(func: T) -> T: + ... + + +def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command can only be used in the context of bot DMs. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + Therefore, there is no error handler called when a command is used within a guild or group DM. + + This decorator can be called with or without parentheses. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.dm_only() + async def my_dm_only_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in DMs!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.dm_channel = True + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING) -> Callable[[T], T]: + """A decorator that indicates this command can only be used in certain contexts. + Valid contexts are guilds, DMs and private channels. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + .. versionadded:: 2.4 + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.allowed_contexts(guilds=True, dms=False, private_channels=True) + async def my_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in guilds and private channels!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + if guilds is not MISSING: + allowed_contexts.guild = guilds + + if dms is not MISSING: + allowed_contexts.dm_channel = dms + + if private_channels is not MISSING: + allowed_contexts.private_channel = private_channels + + return f + + return inner + + +@overload +def guild_install(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def guild_install(func: T) -> T: + ... + + +def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command should be installed in guilds. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + .. versionadded:: 2.4 + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.guild_install() + async def my_guild_install_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in guilds by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + allowed_installs.guild = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +@overload +def user_install(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def user_install(func: T) -> T: + ... + + +def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command should be installed for users. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + .. versionadded:: 2.4 + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.user_install() + async def my_user_install_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in users by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + allowed_installs.user = True + return f # Check if called with parentheses or not @@ -2433,7 +2778,50 @@ def inner(f: T) -> T: return inner(func) -def default_permissions(**perms: bool) -> Callable[[T], T]: +def allowed_installs( + guilds: bool = MISSING, + users: bool = MISSING, +) -> Callable[[T], T]: + """A decorator that indicates this command should be installed in certain contexts. + Valid contexts are guilds and users. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + .. versionadded:: 2.4 + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.allowed_installs(guilds=False, users=True) + async def my_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in users by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + if guilds is not MISSING: + allowed_installs.guild = guilds + + if users is not MISSING: + allowed_installs.user = users + + return f + + return inner + + +def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: bool) -> Callable[[T], T]: r"""A decorator that sets the default permissions needed to execute this command. When this decorator is used, by default users must have these permissions to execute the command. @@ -2457,8 +2845,12 @@ def default_permissions(**perms: bool) -> Callable[[T], T]: ----------- \*\*perms: :class:`bool` Keyword arguments denoting the permissions to set as the default. + perms_obj: :class:`~discord.Permissions` + A permissions object as positional argument. This can be used in combination with ``**perms``. - Example + .. versionadded:: 2.5 + + Examples --------- .. code-block:: python3 @@ -2467,9 +2859,21 @@ def default_permissions(**perms: bool) -> Callable[[T], T]: @app_commands.default_permissions(manage_messages=True) async def test(interaction: discord.Interaction): await interaction.response.send_message('You may or may not have manage messages.') + + .. code-block:: python3 + + ADMIN_PERMS = discord.Permissions(administrator=True) + + @app_commands.command() + @app_commands.default_permissions(ADMIN_PERMS, manage_messages=True) + async def test(interaction: discord.Interaction): + await interaction.response.send_message('You may or may not have manage messages.') """ - permissions = Permissions(**perms) + if perms_obj is not None: + permissions = perms_obj | Permissions(**perms) + else: + permissions = Permissions(**perms) def decorator(func: T) -> T: if isinstance(func, (Command, Group, ContextMenu)): diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index dc63f10e8c88..2efb4e5b008b 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -27,7 +27,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union from ..enums import AppCommandOptionType, AppCommandType, Locale -from ..errors import DiscordException, HTTPException, _flatten_error_dict +from ..errors import DiscordException, HTTPException, _flatten_error_dict, MissingApplicationID as MissingApplicationID from ..utils import _human_join __all__ = ( @@ -59,11 +59,6 @@ CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu] -APP_ID_NOT_FOUND = ( - 'Client does not have an application_id set. Either the function was called before on_ready ' - 'was called or application_id was not passed to the Client constructor.' -) - class AppCommandError(DiscordException): """The base exception type for all application command related errors. @@ -422,19 +417,6 @@ def __init__(self, command: Union[Command[Any, ..., Any], ContextMenu, Group]): super().__init__(msg) -class MissingApplicationID(AppCommandError): - """An exception raised when the client does not have an application ID set. - An application ID is required for syncing application commands. - - This inherits from :exc:`~discord.app_commands.AppCommandError`. - - .. versionadded:: 2.0 - """ - - def __init__(self, message: Optional[str] = None): - super().__init__(message or APP_ID_NOT_FOUND) - - def _get_command_error( index: str, inner: Any, @@ -485,6 +467,10 @@ def _get_command_error( if key == 'options': for index, d in remaining.items(): _get_command_error(index, d, children, messages, indent=indent + 2) + elif key == '_errors': + errors = [x.get('message', '') for x in remaining] + + messages.extend(f'{indentation} {message}' for message in errors) else: if isinstance(remaining, dict): try: @@ -493,10 +479,9 @@ def _get_command_error( errors = _flatten_error_dict(remaining, key=key) else: errors = {key: ' '.join(x.get('message', '') for x in inner_errors)} - else: - errors = _flatten_error_dict(remaining, key=key) - messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) + if isinstance(errors, dict): + messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) class CommandSyncFailure(AppCommandError, HTTPException): diff --git a/discord/app_commands/installs.py b/discord/app_commands/installs.py new file mode 100644 index 000000000000..5ac033245ab7 --- /dev/null +++ b/discord/app_commands/installs.py @@ -0,0 +1,207 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence + +__all__ = ( + 'AppInstallationType', + 'AppCommandContext', +) + +if TYPE_CHECKING: + from typing_extensions import Self + from ..types.interactions import InteractionContextType, InteractionInstallationType + + +class AppInstallationType: + r"""Represents the installation location of an application command. + + .. versionadded:: 2.4 + + Parameters + ----------- + guild: Optional[:class:`bool`] + Whether the integration is a guild install. + user: Optional[:class:`bool`] + Whether the integration is a user install. + """ + + __slots__ = ('_guild', '_user') + + GUILD: ClassVar[int] = 0 + USER: ClassVar[int] = 1 + + def __init__(self, *, guild: Optional[bool] = None, user: Optional[bool] = None): + self._guild: Optional[bool] = guild + self._user: Optional[bool] = user + + @property + def guild(self) -> bool: + """:class:`bool`: Whether the integration is a guild install.""" + return bool(self._guild) + + @guild.setter + def guild(self, value: bool) -> None: + self._guild = bool(value) + + @property + def user(self) -> bool: + """:class:`bool`: Whether the integration is a user install.""" + return bool(self._user) + + @user.setter + def user(self, value: bool) -> None: + self._user = bool(value) + + def merge(self, other: AppInstallationType) -> AppInstallationType: + # Merging is similar to AllowedMentions where `self` is the base + # and the `other` is the override preference + guild = self._guild if other._guild is None else other._guild + user = self._user if other._user is None else other._user + return AppInstallationType(guild=guild, user=user) + + def _is_unset(self) -> bool: + return all(x is None for x in (self._guild, self._user)) + + def _merge_to_array(self, other: Optional[AppInstallationType]) -> Optional[List[InteractionInstallationType]]: + result = self.merge(other) if other is not None else self + if result._is_unset(): + return None + return result.to_array() + + @classmethod + def _from_value(cls, value: Sequence[InteractionInstallationType]) -> Self: + self = cls() + for x in value: + if x == cls.GUILD: + self._guild = True + elif x == cls.USER: + self._user = True + return self + + def to_array(self) -> List[InteractionInstallationType]: + values = [] + if self._guild: + values.append(self.GUILD) + if self._user: + values.append(self.USER) + return values + + +class AppCommandContext: + r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context. + + .. versionadded:: 2.4 + + Parameters + ----------- + guild: Optional[:class:`bool`] + Whether the context allows usage in a guild. + dm_channel: Optional[:class:`bool`] + Whether the context allows usage in a DM channel. + private_channel: Optional[:class:`bool`] + Whether the context allows usage in a DM or a GDM channel. + """ + + GUILD: ClassVar[int] = 0 + DM_CHANNEL: ClassVar[int] = 1 + PRIVATE_CHANNEL: ClassVar[int] = 2 + + __slots__ = ('_guild', '_dm_channel', '_private_channel') + + def __init__( + self, + *, + guild: Optional[bool] = None, + dm_channel: Optional[bool] = None, + private_channel: Optional[bool] = None, + ): + self._guild: Optional[bool] = guild + self._dm_channel: Optional[bool] = dm_channel + self._private_channel: Optional[bool] = private_channel + + @property + def guild(self) -> bool: + """:class:`bool`: Whether the context allows usage in a guild.""" + return bool(self._guild) + + @guild.setter + def guild(self, value: bool) -> None: + self._guild = bool(value) + + @property + def dm_channel(self) -> bool: + """:class:`bool`: Whether the context allows usage in a DM channel.""" + return bool(self._dm_channel) + + @dm_channel.setter + def dm_channel(self, value: bool) -> None: + self._dm_channel = bool(value) + + @property + def private_channel(self) -> bool: + """:class:`bool`: Whether the context allows usage in a DM or a GDM channel.""" + return bool(self._private_channel) + + @private_channel.setter + def private_channel(self, value: bool) -> None: + self._private_channel = bool(value) + + def merge(self, other: AppCommandContext) -> AppCommandContext: + guild = self._guild if other._guild is None else other._guild + dm_channel = self._dm_channel if other._dm_channel is None else other._dm_channel + private_channel = self._private_channel if other._private_channel is None else other._private_channel + return AppCommandContext(guild=guild, dm_channel=dm_channel, private_channel=private_channel) + + def _is_unset(self) -> bool: + return all(x is None for x in (self._guild, self._dm_channel, self._private_channel)) + + def _merge_to_array(self, other: Optional[AppCommandContext]) -> Optional[List[InteractionContextType]]: + result = self.merge(other) if other is not None else self + if result._is_unset(): + return None + return result.to_array() + + @classmethod + def _from_value(cls, value: Sequence[InteractionContextType]) -> Self: + self = cls() + for x in value: + if x == cls.GUILD: + self._guild = True + elif x == cls.DM_CHANNEL: + self._dm_channel = True + elif x == cls.PRIVATE_CHANNEL: + self._private_channel = True + return self + + def to_array(self) -> List[InteractionContextType]: + values = [] + if self._guild: + values.append(self.GUILD) + if self._dm_channel: + values.append(self.DM_CHANNEL) + if self._private_channel: + values.append(self.PRIVATE_CHANNEL) + return values diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index 3e9d250b283b..e8a96784b87c 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -26,9 +26,17 @@ from datetime import datetime from .errors import MissingApplicationID +from ..flags import AppCommandContext, AppInstallationType from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator from ..permissions import Permissions -from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum +from ..enums import ( + AppCommandOptionType, + AppCommandType, + AppCommandPermissionType, + ChannelType, + Locale, + try_enum, +) from ..mixins import Hashable from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING from ..object import Object @@ -160,6 +168,14 @@ class AppCommand(Hashable): The default member permissions that can run this command. dm_permission: :class:`bool` A boolean that indicates whether this command can be run in direct messages. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this command is allowed to be used in. Overrides the ``dm_permission`` attribute. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that this command is allowed to be installed in. + + .. versionadded:: 2.4 guild_id: Optional[:class:`int`] The ID of the guild this command is registered in. A value of ``None`` denotes that it is a global command. @@ -179,6 +195,8 @@ class AppCommand(Hashable): 'options', 'default_member_permissions', 'dm_permission', + 'allowed_contexts', + 'allowed_installs', 'nsfw', '_state', ) @@ -210,6 +228,19 @@ def _from_data(self, data: ApplicationCommandPayload) -> None: dm_permission = True self.dm_permission: bool = dm_permission + + allowed_contexts = data.get('contexts') + if allowed_contexts is None: + self.allowed_contexts: Optional[AppCommandContext] = None + else: + self.allowed_contexts = AppCommandContext._from_value(allowed_contexts) + + allowed_installs = data.get('integration_types') + if allowed_installs is None: + self.allowed_installs: Optional[AppInstallationType] = None + else: + self.allowed_installs = AppInstallationType._from_value(allowed_installs) + self.nsfw: bool = data.get('nsfw', False) self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {}) self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {}) @@ -223,6 +254,8 @@ def to_dict(self) -> ApplicationCommandPayload: 'description': self.description, 'name_localizations': {str(k): v for k, v in self.name_localizations.items()}, 'description_localizations': {str(k): v for k, v in self.description_localizations.items()}, + 'contexts': self.allowed_contexts.to_array() if self.allowed_contexts is not None else None, + 'integration_types': self.allowed_installs.to_array() if self.allowed_installs is not None else None, 'options': [opt.to_dict() for opt in self.options], } # type: ignore # Type checker does not understand this literal. diff --git a/discord/app_commands/namespace.py b/discord/app_commands/namespace.py index 7fad617c679c..3fa81712cff3 100644 --- a/discord/app_commands/namespace.py +++ b/discord/app_commands/namespace.py @@ -179,7 +179,7 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - state = interaction._state members = resolved.get('members', {}) guild_id = interaction.guild_id - guild = state._get_or_create_unavailable_guild(guild_id) if guild_id is not None else None + guild = interaction.guild type = AppCommandOptionType.user.value for (user_id, user_data) in resolved.get('users', {}).items(): try: @@ -220,7 +220,6 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - } ) - guild = state._get_guild(guild_id) for (message_id, message_data) in resolved.get('messages', {}).items(): channel_id = int(message_data['channel_id']) if guild is None: @@ -232,6 +231,7 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - # Type checker doesn't understand this due to failure to narrow message = Message(state=state, channel=channel, data=message_data) # type: ignore + message.guild = guild key = ResolveKey(id=message_id, type=-1) completed[key] = message diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index 59b3af758310..c18485d8c0b4 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -34,6 +34,7 @@ ClassVar, Coroutine, Dict, + Generic, List, Literal, Optional, @@ -56,6 +57,7 @@ from ..role import Role from ..member import Member from ..message import Attachment +from .._types import ClientT __all__ = ( 'Transformer', @@ -191,7 +193,7 @@ def display_name(self) -> str: return self.name if self._rename is MISSING else str(self._rename) -class Transformer: +class Transformer(Generic[ClientT]): """The base class that allows a type annotation in an application command parameter to map into a :class:`~discord.AppCommandOptionType` and transform the raw value into one from this type. @@ -233,7 +235,7 @@ def __call__(self) -> None: pass def __or__(self, rhs: Any) -> Any: - return Union[self, rhs] # type: ignore + return Union[self, rhs] @property def type(self) -> AppCommandOptionType: @@ -304,7 +306,7 @@ def _error_display_name(self) -> str: else: return name - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: """|maybecoro| Transforms the converted option value into another value. @@ -324,7 +326,7 @@ async def transform(self, interaction: Interaction, value: Any, /) -> Any: raise NotImplementedError('Derived classes need to implement this.') async def autocomplete( - self, interaction: Interaction, value: Union[int, float, str], / + self, interaction: Interaction[ClientT], value: Union[int, float, str], / ) -> List[Choice[Union[int, float, str]]]: """|coro| @@ -352,7 +354,7 @@ async def autocomplete( raise NotImplementedError('Derived classes can implement this.') -class IdentityTransformer(Transformer): +class IdentityTransformer(Transformer[ClientT]): def __init__(self, type: AppCommandOptionType) -> None: self._type = type @@ -360,7 +362,7 @@ def __init__(self, type: AppCommandOptionType) -> None: def type(self) -> AppCommandOptionType: return self._type - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: return value @@ -489,7 +491,7 @@ async def transform(self, interaction: Interaction, value: Any, /) -> Any: return self._enum[value] -class InlineTransformer(Transformer): +class InlineTransformer(Transformer[ClientT]): def __init__(self, annotation: Any) -> None: super().__init__() self.annotation: Any = annotation @@ -502,7 +504,7 @@ def _error_display_name(self) -> str: def type(self) -> AppCommandOptionType: return AppCommandOptionType.string - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: return await self.annotation.transform(interaction, value) @@ -611,18 +613,18 @@ def __class_getitem__(cls, obj) -> RangeTransformer: return transformer -class MemberTransformer(Transformer): +class MemberTransformer(Transformer[ClientT]): @property def type(self) -> AppCommandOptionType: return AppCommandOptionType.user - async def transform(self, interaction: Interaction, value: Any, /) -> Member: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Member: if not isinstance(value, Member): raise TransformerError(value, self.type, self) return value -class BaseChannelTransformer(Transformer): +class BaseChannelTransformer(Transformer[ClientT]): def __init__(self, *channel_types: Type[Any]) -> None: super().__init__() if len(channel_types) == 1: @@ -638,7 +640,7 @@ def __init__(self, *channel_types: Type[Any]) -> None: except KeyError: raise TypeError('Union type of channels must be entirely made up of channels') from None - self._types: Tuple[Type[Any]] = channel_types + self._types: Tuple[Type[Any], ...] = channel_types self._channel_types: List[ChannelType] = types self._display_name = display_name @@ -654,22 +656,22 @@ def type(self) -> AppCommandOptionType: def channel_types(self) -> List[ChannelType]: return self._channel_types - async def transform(self, interaction: Interaction, value: Any, /): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): resolved = value.resolve() if resolved is None or not isinstance(resolved, self._types): raise TransformerError(value, AppCommandOptionType.channel, self) return resolved -class RawChannelTransformer(BaseChannelTransformer): - async def transform(self, interaction: Interaction, value: Any, /): +class RawChannelTransformer(BaseChannelTransformer[ClientT]): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): if not isinstance(value, self._types): raise TransformerError(value, AppCommandOptionType.channel, self) return value -class UnionChannelTransformer(BaseChannelTransformer): - async def transform(self, interaction: Interaction, value: Any, /): +class UnionChannelTransformer(BaseChannelTransformer[ClientT]): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): if isinstance(value, self._types): return value @@ -780,11 +782,11 @@ def get_supported_annotation( # Check if there's an origin origin = getattr(annotation, '__origin__', None) if origin is Literal: - args = annotation.__args__ # type: ignore + args = annotation.__args__ return (LiteralTransformer(args), MISSING, True) if origin is Choice: - arg = annotation.__args__[0] # type: ignore + arg = annotation.__args__[0] return (ChoiceTransformer(arg), MISSING, True) if origin is not Union: @@ -792,7 +794,7 @@ def get_supported_annotation( raise TypeError(f'unsupported type annotation {annotation!r}') default = MISSING - args = annotation.__args__ # type: ignore + args = annotation.__args__ if args[-1] is _none: if len(args) == 2: underlying = args[0] diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index c75682e0ea2c..3099071c01e0 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -58,6 +58,7 @@ CommandSyncFailure, MissingApplicationID, ) +from .installs import AppCommandContext, AppInstallationType from .translator import Translator, locale_str from ..errors import ClientException, HTTPException from ..enums import AppCommandType, InteractionType @@ -72,7 +73,7 @@ from .commands import ContextMenuCallback, CommandCallback, P, T ErrorFunc = Callable[ - [Interaction, AppCommandError], + [Interaction[ClientT], AppCommandError], Coroutine[Any, Any, Any], ] @@ -121,9 +122,26 @@ class CommandTree(Generic[ClientT]): to find the guild-specific ``/ping`` command it will fall back to the global ``/ping`` command. This has the potential to raise more :exc:`~discord.app_commands.CommandSignatureMismatch` errors than usual. Defaults to ``True``. + allowed_contexts: :class:`~discord.app_commands.AppCommandContext` + The default allowed contexts that applies to all commands in this tree. + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 + allowed_installs: :class:`~discord.app_commands.AppInstallationType` + The default allowed install locations that apply to all commands in this tree. + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 """ - def __init__(self, client: ClientT, *, fallback_to_global: bool = True): + def __init__( + self, + client: ClientT, + *, + fallback_to_global: bool = True, + allowed_contexts: AppCommandContext = MISSING, + allowed_installs: AppInstallationType = MISSING, + ): self.client: ClientT = client self._http = client.http self._state = client._connection @@ -133,6 +151,8 @@ def __init__(self, client: ClientT, *, fallback_to_global: bool = True): self._state._command_tree = self self.fallback_to_global: bool = fallback_to_global + self.allowed_contexts = AppCommandContext() if allowed_contexts is MISSING else allowed_contexts + self.allowed_installs = AppInstallationType() if allowed_installs is MISSING else allowed_installs self._guild_commands: Dict[int, Dict[str, Union[Command, Group]]] = {} self._global_commands: Dict[str, Union[Command, Group]] = {} # (name, guild_id, command_type): Command @@ -287,10 +307,24 @@ def add_command( guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + override: :class:`bool` Whether to override a command with the same name. If ``False`` an exception is raised. Default is ``False``. @@ -722,7 +756,7 @@ def walk_commands( else: guild_id = None if guild is None else guild.id value = type.value - for ((_, g, t), command) in self._context_menus.items(): + for (_, g, t), command in self._context_menus.items(): if g == guild_id and t == value: yield command @@ -799,7 +833,7 @@ async def on_error(self, interaction: Interaction[ClientT], error: AppCommandErr else: _log.error('Ignoring exception in command tree', exc_info=error) - def error(self, coro: ErrorFunc) -> ErrorFunc: + def error(self, coro: ErrorFunc[ClientT]) -> ErrorFunc[ClientT]: """A decorator that registers a coroutine as a local error handler. This must match the signature of the :meth:`on_error` callback. @@ -825,7 +859,7 @@ def error(self, coro: ErrorFunc) -> ErrorFunc: if len(params) != 2: raise TypeError('error handler must have 2 parameters') - self.on_error = coro + self.on_error = coro # type: ignore return coro def command( @@ -857,10 +891,24 @@ def command( guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + auto_locale_strings: :class:`bool` If this is set to ``True``, then all translatable strings will implicitly be wrapped into :class:`locale_str` rather than :class:`str`. This could @@ -940,10 +988,24 @@ async def ban(interaction: discord.Interaction, user: discord.Member): guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + auto_locale_strings: :class:`bool` If this is set to ``True``, then all translatable strings will implicitly be wrapped into :class:`locale_str` rather than :class:`str`. This could @@ -1058,9 +1120,9 @@ async def sync(self, *, guild: Optional[Snowflake] = None) -> List[AppCommand]: translator = self.translator if translator: - payload = [await command.get_translated_payload(translator) for command in commands] + payload = [await command.get_translated_payload(self, translator) for command in commands] else: - payload = [command.to_dict() for command in commands] + payload = [command.to_dict(self) for command in commands] try: if guild is None: diff --git a/discord/appinfo.py b/discord/appinfo.py index 79be2035f8fd..990c7c2fe356 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -24,7 +24,7 @@ from __future__ import annotations -from typing import List, TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING, Literal, Optional from . import utils from .asset import Asset @@ -41,6 +41,7 @@ PartialAppInfo as PartialAppInfoPayload, Team as TeamPayload, InstallParams as InstallParamsPayload, + AppIntegrationTypeConfig as AppIntegrationTypeConfigPayload, ) from .user import User from .state import ConnectionState @@ -49,6 +50,7 @@ 'AppInfo', 'PartialAppInfo', 'AppInstallParams', + 'IntegrationTypeConfig', ) @@ -143,6 +145,14 @@ class AppInfo: A list of authentication redirect URIs. .. versionadded:: 2.4 + approximate_guild_count: :class:`int` + The approximate count of the guilds the bot was added to. + + .. versionadded:: 2.4 + approximate_user_install_count: Optional[:class:`int`] + The approximate count of the user-level installations the bot has. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -170,6 +180,9 @@ class AppInfo: 'role_connections_verification_url', 'interactions_endpoint_url', 'redirect_uris', + 'approximate_guild_count', + 'approximate_user_install_count', + '_integration_types_config', ) def __init__(self, state: ConnectionState, data: AppInfoPayload): @@ -206,6 +219,11 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.install_params: Optional[AppInstallParams] = AppInstallParams(params) if params else None self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url') self.redirect_uris: List[str] = data.get('redirect_uris', []) + self.approximate_guild_count: int = data.get('approximate_guild_count', 0) + self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count') + self._integration_types_config: Dict[Literal['0', '1'], AppIntegrationTypeConfigPayload] = data.get( + 'integration_types_config', {} + ) def __repr__(self) -> str: return ( @@ -248,6 +266,36 @@ def flags(self) -> ApplicationFlags: """ return ApplicationFlags._from_value(self._flags) + @property + def guild_integration_config(self) -> Optional[IntegrationTypeConfig]: + """Optional[:class:`IntegrationTypeConfig`]: The default settings for the + application's installation context in a guild. + + .. versionadded:: 2.5 + """ + if not self._integration_types_config: + return None + + try: + return IntegrationTypeConfig(self._integration_types_config['0']) + except KeyError: + return None + + @property + def user_integration_config(self) -> Optional[IntegrationTypeConfig]: + """Optional[:class:`IntegrationTypeConfig`]: The default settings for the + application's installation context as a user. + + .. versionadded:: 2.5 + """ + if not self._integration_types_config: + return None + + try: + return IntegrationTypeConfig(self._integration_types_config['1']) + except KeyError: + return None + async def edit( self, *, @@ -262,6 +310,10 @@ async def edit( cover_image: Optional[bytes] = MISSING, interactions_endpoint_url: Optional[str] = MISSING, tags: Optional[List[str]] = MISSING, + guild_install_scopes: Optional[List[str]] = MISSING, + guild_install_permissions: Optional[Permissions] = MISSING, + user_install_scopes: Optional[List[str]] = MISSING, + user_install_permissions: Optional[Permissions] = MISSING, ) -> AppInfo: r"""|coro| @@ -303,6 +355,24 @@ async def edit( over the gateway. Can be ``None`` to remove the URL. tags: Optional[List[:class:`str`]] The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags. + guild_install_scopes: Optional[List[:class:`str`]] + The new list of :ddocs:`OAuth2 scopes ` of + the default guild installation context. Can be ``None`` to remove the scopes. + + .. versionadded: 2.5 + guild_install_permissions: Optional[:class:`Permissions`] + The new permissions of the default guild installation context. Can be ``None`` to remove the permissions. + + .. versionadded: 2.5 + user_install_scopes: Optional[List[:class:`str`]] + The new list of :ddocs:`OAuth2 scopes ` of + the default user installation context. Can be ``None`` to remove the scopes. + + .. versionadded: 2.5 + user_install_permissions: Optional[:class:`Permissions`] + The new permissions of the default user installation context. Can be ``None`` to remove the permissions. + + .. versionadded: 2.5 reason: Optional[:class:`str`] The reason for editing the application. Shows up on the audit log. @@ -312,7 +382,8 @@ async def edit( Editing the application failed ValueError The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised - when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other. + when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other, + or when ``guild_install_scopes`` and ``guild_install_permissions`` are incompatible with each other. Returns ------- @@ -352,7 +423,7 @@ async def edit( else: if install_params_permissions is not MISSING: - raise ValueError("install_params_scopes must be set if install_params_permissions is set") + raise ValueError('install_params_scopes must be set if install_params_permissions is set') if flags is not MISSING: if flags is None: @@ -377,6 +448,51 @@ async def edit( if tags is not MISSING: payload['tags'] = tags + + integration_types_config: Dict[str, Any] = {} + if guild_install_scopes is not MISSING or guild_install_permissions is not MISSING: + guild_install_params: Optional[Dict[str, Any]] = {} + if guild_install_scopes in (None, MISSING): + guild_install_scopes = [] + + if 'bot' not in guild_install_scopes and guild_install_permissions is not MISSING: + raise ValueError("'bot' must be in guild_install_scopes if guild_install_permissions is set") + + if guild_install_permissions in (None, MISSING): + guild_install_params['permissions'] = 0 + else: + guild_install_params['permissions'] = guild_install_permissions.value + + guild_install_params['scopes'] = guild_install_scopes + + integration_types_config['0'] = {'oauth2_install_params': guild_install_params or None} + else: + if guild_install_permissions is not MISSING: + raise ValueError('guild_install_scopes must be set if guild_install_permissions is set') + + if user_install_scopes is not MISSING or user_install_permissions is not MISSING: + user_install_params: Optional[Dict[str, Any]] = {} + if user_install_scopes in (None, MISSING): + user_install_scopes = [] + + if 'bot' not in user_install_scopes and user_install_permissions is not MISSING: + raise ValueError("'bot' must be in user_install_scopes if user_install_permissions is set") + + if user_install_permissions in (None, MISSING): + user_install_params['permissions'] = 0 + else: + user_install_params['permissions'] = user_install_permissions.value + + user_install_params['scopes'] = user_install_scopes + + integration_types_config['1'] = {'oauth2_install_params': user_install_params or None} + else: + if user_install_permissions is not MISSING: + raise ValueError('user_install_scopes must be set if user_install_permissions is set') + + if integration_types_config: + payload['integration_types_config'] = integration_types_config + data = await self._state.http.edit_application_info(reason=reason, payload=payload) return AppInfo(data=data, state=self._state) @@ -508,3 +624,22 @@ class AppInstallParams: def __init__(self, data: InstallParamsPayload) -> None: self.scopes: List[str] = data.get('scopes', []) self.permissions: Permissions = Permissions(int(data['permissions'])) + + +class IntegrationTypeConfig: + """Represents the default settings for the application's installation context. + + .. versionadded:: 2.5 + + Attributes + ---------- + oauth2_install_params: Optional[:class:`AppInstallParams`] + The install params for this installation context's default in-app authorization link. + """ + + def __init__(self, data: AppIntegrationTypeConfigPayload) -> None: + self.oauth2_install_params: Optional[AppInstallParams] = None + try: + self.oauth2_install_params = AppInstallParams(data['oauth2_install_params']) # type: ignore # EAFP + except KeyError: + pass diff --git a/discord/asset.py b/discord/asset.py index d88ebb945d44..e3422f3110d4 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -246,6 +246,26 @@ def _from_guild_avatar(cls, state: _State, guild_id: int, member_id: int, avatar animated=animated, ) + @classmethod + def _from_guild_banner(cls, state: _State, guild_id: int, member_id: int, banner: str) -> Self: + animated = banner.startswith('a_') + format = 'gif' if animated else 'png' + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024", + key=banner, + animated=animated, + ) + + @classmethod + def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self: + return cls( + state, + url=f'{cls.BASE}/avatar-decoration-presets/{avatar_decoration}.png?size=96', + key=avatar_decoration, + animated=True, + ) + @classmethod def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self: return cls( @@ -420,7 +440,7 @@ def replace( url = url.with_query(url.raw_query_string) url = str(url) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_size(self, size: int, /) -> Self: """Returns a new asset with the specified size. @@ -448,7 +468,7 @@ def with_size(self, size: int, /) -> Self: raise ValueError('size must be a power of 2 between 16 and 4096') url = str(yarl.URL(self._url).with_query(size=size)) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_format(self, format: ValidAssetFormatTypes, /) -> Self: """Returns a new asset with the specified format. @@ -483,7 +503,7 @@ def with_format(self, format: ValidAssetFormatTypes, /) -> Self: url = yarl.URL(self._url) path, _ = os.path.splitext(url.path) url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string)) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_static_format(self, format: ValidStaticFormatTypes, /) -> Self: """Returns a new asset with the specified static format. diff --git a/discord/audit_logs.py b/discord/audit_logs.py index fc1bc298b602..af67855d4584 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -235,6 +235,10 @@ def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAc return [AutoModRuleAction.from_data(action) for action in data] +def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji: + return PartialEmoji(name=data) + + E = TypeVar('E', bound=enums.Enum) @@ -341,6 +345,8 @@ class AuditLogChanges: 'available_tags': (None, _transform_forum_tags), 'flags': (None, _transform_overloaded_flags), 'default_reaction_emoji': (None, _transform_default_reaction), + 'emoji_name': ('emoji', _transform_default_emoji), + 'user_id': ('user', _transform_member_id) } # fmt: on @@ -868,7 +874,13 @@ def _convert_target_invite(self, target_id: None) -> Invite: def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]: return self._state.get_emoji(target_id) or Object(id=target_id, type=Emoji) - def _convert_target_message(self, target_id: int) -> Union[Member, User, Object]: + def _convert_target_message(self, target_id: Optional[int]) -> Optional[Union[Member, User, Object]]: + # The message_pin and message_unpin action types do not have a + # non-null target_id so safeguard against that + + if target_id is None: + return None + return self._get_member(target_id) or Object(id=target_id, type=Member) def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]: diff --git a/discord/automod.py b/discord/automod.py index b20f9016a12a..61683c269134 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -518,7 +518,7 @@ async def edit( payload['name'] = name if event_type is not MISSING: - payload['event_type'] = event_type + payload['event_type'] = event_type.value if trigger is not MISSING: trigger_metadata = trigger.to_metadata_dict() @@ -541,7 +541,7 @@ async def edit( **payload, ) - return AutoModRule(data=data, guild=self.guild, state=self._state) + return self.__class__(data=data, guild=self.guild, state=self._state) async def delete(self, *, reason: str = MISSING) -> None: """|coro| diff --git a/discord/channel.py b/discord/channel.py index 52bb4706903b..168eee1f4ad6 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -47,7 +47,16 @@ import discord.abc from .scheduled_event import ScheduledEvent from .permissions import PermissionOverwrite, Permissions -from .enums import ChannelType, ForumLayoutType, ForumOrderType, PrivacyLevel, try_enum, VideoQualityMode, EntityType +from .enums import ( + ChannelType, + ForumLayoutType, + ForumOrderType, + PrivacyLevel, + try_enum, + VideoQualityMode, + EntityType, + VoiceChannelEffectAnimationType, +) from .mixins import Hashable from . import utils from .utils import MISSING @@ -56,8 +65,10 @@ from .stage_instance import StageInstance from .threads import Thread from .partial_emoji import _EmojiTag, PartialEmoji -from .flags import ChannelFlags +from .flags import ChannelFlags, MessageFlags from .http import handle_message_parameters +from .object import Object +from .soundboard import BaseSoundboardSound, SoundboardDefaultSound __all__ = ( 'TextChannel', @@ -69,6 +80,8 @@ 'ForumChannel', 'GroupChannel', 'PartialMessageable', + 'VoiceChannelEffect', + 'VoiceChannelSoundEffect', ) if TYPE_CHECKING: @@ -76,7 +89,6 @@ from .types.threads import ThreadArchiveDuration from .role import Role - from .object import Object from .member import Member, VoiceState from .abc import Snowflake, SnowflakeTime from .embeds import Embed @@ -88,7 +100,7 @@ from .file import File from .user import ClientUser, User, BaseUser from .guild import Guild, GuildChannel as GuildChannelType - from .ui.view import View + from .ui.view import BaseView, View, LayoutView from .types.channel import ( TextChannel as TextChannelPayload, NewsChannel as NewsChannelPayload, @@ -100,8 +112,11 @@ ForumChannel as ForumChannelPayload, MediaChannel as MediaChannelPayload, ForumTag as ForumTagPayload, + VoiceChannelEffect as VoiceChannelEffectPayload, ) from .types.snowflake import SnowflakeList + from .types.soundboard import BaseSoundboardSound as BaseSoundboardSoundPayload + from .soundboard import SoundboardSound OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object]) @@ -111,6 +126,121 @@ class ThreadWithMessage(NamedTuple): message: Message +class VoiceChannelEffectAnimation(NamedTuple): + id: int + type: VoiceChannelEffectAnimationType + + +class VoiceChannelSoundEffect(BaseSoundboardSound): + """Represents a Discord voice channel sound effect. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sound effects are equal. + + .. describe:: x != y + + Checks if two sound effects are not equal. + + .. describe:: hash(x) + + Returns the sound effect's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + """ + + __slots__ = ('_state',) + + def __init__(self, *, state: ConnectionState, id: int, volume: float): + data: BaseSoundboardSoundPayload = { + 'sound_id': id, + 'volume': volume, + } + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} volume={self.volume}>" + + @property + def created_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the snowflake's creation time in UTC. + Returns ``None`` if it's a default sound.""" + if self.is_default(): + return None + else: + return utils.snowflake_time(self.id) + + def is_default(self) -> bool: + """:class:`bool`: Whether it's a default sound or not.""" + # if it's smaller than the Discord Epoch it cannot be a snowflake + return self.id < utils.DISCORD_EPOCH + + +class VoiceChannelEffect: + """Represents a Discord voice channel effect. + + .. versionadded:: 2.5 + + Attributes + ------------ + channel: :class:`VoiceChannel` + The channel in which the effect is sent. + user: Optional[:class:`Member`] + The user who sent the effect. ``None`` if not found in cache. + animation: Optional[:class:`VoiceChannelEffectAnimation`] + The animation the effect has. Returns ``None`` if the effect has no animation. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the effect. + sound: Optional[:class:`VoiceChannelSoundEffect`] + The sound of the effect. Returns ``None`` if it's an emoji effect. + """ + + __slots__ = ('channel', 'user', 'animation', 'emoji', 'sound') + + def __init__(self, *, state: ConnectionState, data: VoiceChannelEffectPayload, guild: Guild): + self.channel: VoiceChannel = guild.get_channel(int(data['channel_id'])) # type: ignore # will always be a VoiceChannel + self.user: Optional[Member] = guild.get_member(int(data['user_id'])) + self.animation: Optional[VoiceChannelEffectAnimation] = None + + animation_id = data.get('animation_id') + if animation_id is not None: + animation_type = try_enum(VoiceChannelEffectAnimationType, data['animation_type']) # type: ignore # cannot be None here + self.animation = VoiceChannelEffectAnimation(id=animation_id, type=animation_type) + + emoji = data.get('emoji') + self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None + self.sound: Optional[VoiceChannelSoundEffect] = None + + sound_id: Optional[int] = utils._get_as_snowflake(data, 'sound_id') + if sound_id is not None: + sound_volume = data.get('sound_volume') or 0.0 + self.sound = VoiceChannelSoundEffect(state=state, id=sound_id, volume=sound_volume) + + def __repr__(self) -> str: + attrs = [ + ('channel', self.channel), + ('user', self.user), + ('animation', self.animation), + ('emoji', self.emoji), + ('sound', self.sound), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + def is_sound(self) -> bool: + """:class:`bool`: Whether the effect is a sound or not.""" + return self.sound is not None + + class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. @@ -395,9 +525,26 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> TextChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> TextChannel: + base: Dict[Any, Any] = { + 'topic': self.topic, + 'nsfw': self.nsfw, + 'default_auto_archive_duration': self.default_auto_archive_duration, + 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, + } + if not self.is_news(): + base['rate_limit_per_user'] = self.slowmode_delay return await self._clone_impl( - {'topic': self.topic, 'nsfw': self.nsfw, 'rate_limit_per_user': self.slowmode_delay}, name=name, reason=reason + base, + name=name, + category=category, + reason=reason, ) async def delete_messages(self, messages: Iterable[Snowflake], *, reason: Optional[str] = None) -> None: @@ -1249,6 +1396,27 @@ async def create_webhook(self, *, name: str, avatar: Optional[bytes] = None, rea data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason) return Webhook.from_state(data, state=self._state) + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, *, name: Optional[str] = None, category: Optional[CategoryChannel] = None, reason: Optional[str] = None + ) -> Self: + base = { + 'bitrate': self.bitrate, + 'user_limit': self.user_limit, + 'rate_limit_per_user': self.slowmode_delay, + 'nsfw': self.nsfw, + 'video_quality_mode': self.video_quality_mode.value, + } + if self.rtc_region: + base['rtc_region'] = self.rtc_region + + return await self._clone_impl( + base, + name=name, + category=category, + reason=reason, + ) + class VoiceChannel(VocalGuildChannel): """Represents a Discord guild voice channel. @@ -1343,10 +1511,6 @@ def type(self) -> Literal[ChannelType.voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> VoiceChannel: - return await self._clone_impl({'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, reason=reason) - @overload async def edit(self) -> None: ... @@ -1456,6 +1620,35 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona # the payload will always be the proper channel payload return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + async def send_sound(self, sound: Union[SoundboardSound, SoundboardDefaultSound], /) -> None: + """|coro| + + Sends a soundboard sound for this channel. + + You must have :attr:`~Permissions.speak` and :attr:`~Permissions.use_soundboard` to do this. + Additionally, you must have :attr:`~Permissions.use_external_sounds` if the sound is from + a different guild. + + .. versionadded:: 2.5 + + Parameters + ----------- + sound: Union[:class:`SoundboardSound`, :class:`SoundboardDefaultSound`] + The sound to send for this channel. + + Raises + ------- + Forbidden + You do not have permissions to send a sound for this channel. + HTTPException + Sending the sound failed. + """ + payload = {'sound_id': sound.id} + if not isinstance(sound, SoundboardDefaultSound) and self.guild.id != sound.guild.id: + payload['source_guild_id'] = sound.guild.id + + await self._state.http.send_soundboard_sound(self.id, **payload) + class StageChannel(VocalGuildChannel): """Represents a Discord guild stage channel. @@ -1588,10 +1781,6 @@ def type(self) -> Literal[ChannelType.stage_voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.stage_voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> StageChannel: - return await self._clone_impl({}, name=name, reason=reason) - @property def instance(self) -> Optional[StageInstance]: """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. @@ -1702,6 +1891,7 @@ async def edit( *, name: str = ..., nsfw: bool = ..., + bitrate: int = ..., user_limit: int = ..., position: int = ..., sync_permissions: int = ..., @@ -1738,6 +1928,8 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona ---------- name: :class:`str` The new channel's name. + bitrate: :class:`int` + The new channel's bitrate. position: :class:`int` The new channel's position. nsfw: :class:`bool` @@ -1866,7 +2058,13 @@ def is_nsfw(self) -> bool: return self.nsfw @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> CategoryChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> CategoryChannel: return await self._clone_impl({'nsfw': self.nsfw}, name=name, reason=reason) @overload @@ -2294,6 +2492,14 @@ def type(self) -> Literal[ChannelType.forum, ChannelType.media]: def _sorting_bucket(self) -> int: return ChannelType.text.value + @property + def members(self) -> List[Member]: + """List[:class:`Member`]: Returns all members that can see this channel. + + .. versionadded:: 2.5 + """ + return [m for m in self.guild.members if self.permissions_for(m).read_messages] + @property def _scheduled_event_entity_type(self) -> Optional[EntityType]: return None @@ -2383,9 +2589,33 @@ def is_media(self) -> bool: return self._type == ChannelType.media.value @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> ForumChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel], + reason: Optional[str] = None, + ) -> ForumChannel: + base = { + 'topic': self.topic, + 'rate_limit_per_user': self.slowmode_delay, + 'nsfw': self.nsfw, + 'default_auto_archive_duration': self.default_auto_archive_duration, + 'available_tags': [tag.to_dict() for tag in self.available_tags], + 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, + } + if self.default_sort_order: + base['default_sort_order'] = self.default_sort_order.value + if self.default_reaction_emoji: + base['default_reaction_emoji'] = self.default_reaction_emoji._to_forum_tag_payload() + if not self.is_media() and self.default_layout: + base['default_forum_layout'] = self.default_layout.value + return await self._clone_impl( - {'topic': self.topic, 'nsfw': self.nsfw, 'rate_limit_per_user': self.slowmode_delay}, name=name, reason=reason + base, + name=name, + category=category, + reason=reason, ) @overload @@ -2611,6 +2841,46 @@ async def create_tag( return result + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + file: File = ..., + files: Sequence[File] = ..., + allowed_mentions: AllowedMentions = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + reason: Optional[str] = ..., + ) -> ThreadWithMessage: + ... + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + content: Optional[str] = ..., + tts: bool = ..., + embed: Embed = ..., + embeds: Sequence[Embed] = ..., + file: File = ..., + files: Sequence[File] = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + mention_author: bool = ..., + applied_tags: Sequence[ForumTag] = ..., + view: View = ..., + suppress_embeds: bool = ..., + reason: Optional[str] = ..., + ) -> ThreadWithMessage: + ... + async def create_thread( self, *, @@ -2627,7 +2897,7 @@ async def create_thread( allowed_mentions: AllowedMentions = MISSING, mention_author: bool = MISSING, applied_tags: Sequence[ForumTag] = MISSING, - view: View = MISSING, + view: BaseView = MISSING, suppress_embeds: bool = False, reason: Optional[str] = None, ) -> ThreadWithMessage: @@ -2677,8 +2947,11 @@ async def create_thread( If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. applied_tags: List[:class:`discord.ForumTag`] A list of tags to apply to the thread. - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. + + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. suppress_embeds: :class:`bool` @@ -2716,8 +2989,6 @@ async def create_thread( raise TypeError(f'view parameter must be View not {view.__class__.__name__}') if suppress_embeds: - from .message import MessageFlags # circular import - flags = MessageFlags._from_value(4) else: flags = MISSING @@ -2916,22 +3187,21 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable): The user you are participating with in the direct message channel. If this channel is received through the gateway, the recipient information may not be always available. + recipients: List[:class:`User`] + The users you are participating with in the DM channel. + + .. versionadded:: 2.4 me: :class:`ClientUser` The user presenting yourself. id: :class:`int` The direct message channel ID. """ - __slots__ = ('id', 'recipient', 'me', '_state') + __slots__ = ('id', 'recipients', 'me', '_state') def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): self._state: ConnectionState = state - self.recipient: Optional[User] = None - - recipients = data.get('recipients') - if recipients is not None: - self.recipient = state.store_user(recipients[0]) - + self.recipients: List[User] = [state.store_user(u) for u in data.get('recipients', [])] self.me: ClientUser = me self.id: int = int(data['id']) @@ -2951,11 +3221,17 @@ def _from_message(cls, state: ConnectionState, channel_id: int) -> Self: self = cls.__new__(cls) self._state = state self.id = channel_id - self.recipient = None + self.recipients = [] # state.user won't be None here self.me = state.user # type: ignore return self + @property + def recipient(self) -> Optional[User]: + if self.recipients: + return self.recipients[0] + return None + @property def type(self) -> Literal[ChannelType.private]: """:class:`ChannelType`: The channel's Discord type.""" @@ -3304,6 +3580,14 @@ def permissions_for(self, obj: Any = None, /) -> Permissions: return Permissions.none() + @property + def mention(self) -> str: + """:class:`str`: Returns a string that allows you to mention the channel. + + .. versionadded:: 2.5 + """ + return f'<#{self.id}>' + def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. diff --git a/discord/client.py b/discord/client.py index ccafc073d1ea..c620dc23a9b5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -53,7 +53,7 @@ from .invite import Invite from .template import Template from .widget import Widget -from .guild import Guild +from .guild import Guild, GuildPreview from .emoji import Emoji from .channel import _threaded_channel_factory, PartialMessageable from .enums import ChannelType, EntitlementOwnerType @@ -72,11 +72,12 @@ from .backoff import ExponentialBackoff from .webhook import Webhook from .appinfo import AppInfo -from .ui.view import View +from .ui.view import BaseView from .ui.dynamic import DynamicItem from .stage_instance import StageInstance from .threads import Thread from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory +from .soundboard import SoundboardDefaultSound, SoundboardSound if TYPE_CHECKING: from types import TracebackType @@ -84,7 +85,7 @@ from typing_extensions import Self from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime - from .app_commands import Command, ContextMenu, MissingApplicationID + from .app_commands import Command, ContextMenu from .automod import AutoModAction, AutoModRule from .channel import DMChannel, GroupChannel from .ext.commands import AutoShardedBot, Bot, Context, CommandError @@ -107,6 +108,7 @@ RawThreadMembersUpdate, RawThreadUpdateEvent, RawTypingEvent, + RawPollVoteActionEvent, ) from .reaction import Reaction from .role import Role @@ -116,6 +118,8 @@ from .ui.item import Item from .voice_client import VoiceProtocol from .audit_logs import AuditLogEntry + from .poll import PollAnswer + from .subscription import Subscription # fmt: off @@ -233,6 +237,15 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 + enable_raw_presences: :class:`bool` + Whether to manually enable or disable the :func:`on_raw_presence_update` event. + + Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. + + By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` + is disabled, otherwise it's set to ``False``. + + .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``. This allows you to check requests the library is using. For more information, check the @@ -247,6 +260,11 @@ class Client: set to is ``30.0`` seconds. .. versionadded:: 2.0 + connector: Optional[:class:`aiohttp.BaseConnector`] + The aiohttp connector to use for this client. This can be used to control underlying aiohttp + behavior, such as setting a dns resolver or sslcontext. + + .. versionadded:: 2.5 Attributes ----------- @@ -262,6 +280,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: self.shard_id: Optional[int] = options.get('shard_id') self.shard_count: Optional[int] = options.get('shard_count') + connector: Optional[aiohttp.BaseConnector] = options.get('connector', None) proxy: Optional[str] = options.pop('proxy', None) proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None) unsync_clock: bool = options.pop('assume_unsync_clock', True) @@ -269,6 +288,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None) self.http: HTTPClient = HTTPClient( self.loop, + connector, proxy=proxy, proxy_auth=proxy_auth, unsync_clock=unsync_clock, @@ -287,7 +307,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: self._enable_debug_events: bool = options.pop('enable_debug_events', False) self._connection: ConnectionState[Self] = self._get_state(intents=intents, **options) self._connection.shard_count = self.shard_count - self._closed: bool = False + self._closing_task: Optional[asyncio.Task[None]] = None self._ready: asyncio.Event = MISSING self._application: Optional[AppInfo] = None self._connection._get_websocket = self._get_websocket @@ -307,7 +327,10 @@ async def __aexit__( exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: - if not self.is_closed(): + # This avoids double-calling a user-provided .close() + if self._closing_task: + await self._closing_task + else: await self.close() # internals @@ -315,7 +338,7 @@ async def __aexit__( def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket: return self.ws - def _get_state(self, **options: Any) -> ConnectionState: + def _get_state(self, **options: Any) -> ConnectionState[Self]: return ConnectionState(dispatch=self.dispatch, handlers=self._handlers, hooks=self._hooks, http=self.http, **options) def _handle_ready(self) -> None: @@ -354,7 +377,13 @@ def guilds(self) -> Sequence[Guild]: @property def emojis(self) -> Sequence[Emoji]: - """Sequence[:class:`.Emoji`]: The emojis that the connected client has.""" + """Sequence[:class:`.Emoji`]: The emojis that the connected client has. + + .. note:: + + This not include the emojis that are owned by the application. + Use :meth:`.fetch_application_emoji` to get those. + """ return self._connection.emojis @property @@ -365,6 +394,14 @@ def stickers(self) -> Sequence[GuildSticker]: """ return self._connection.stickers + @property + def soundboard_sounds(self) -> List[SoundboardSound]: + """List[:class:`.SoundboardSound`]: The soundboard sounds that the connected client has. + + .. versionadded:: 2.5 + """ + return self._connection.soundboard_sounds + @property def cached_messages(self) -> Sequence[Message]: """Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached. @@ -618,6 +655,11 @@ async def login(self, token: str) -> None: if self._connection.application_id is None: self._connection.application_id = self._application.id + if self._application.interactions_endpoint_url is not None: + _log.warning( + 'Application has an interaction endpoint URL set, this means registered components and app commands will not be received by the library.' + ) + if not self._connection.application_flags: self._connection.application_flags = self._application.flags @@ -726,22 +768,24 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self._closed: - return + if self._closing_task: + return await self._closing_task - self._closed = True + async def _close(): + await self._connection.close() - await self._connection.close() + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) - if self.ws is not None and self.ws.open: - await self.ws.close(code=1000) + await self.http.close() - await self.http.close() + if self._ready is not MISSING: + self._ready.clear() - if self._ready is not MISSING: - self._ready.clear() + self.loop = MISSING - self.loop = MISSING + self._closing_task = asyncio.create_task(_close()) + await self._closing_task def clear(self) -> None: """Clears the internal state of the bot. @@ -750,7 +794,7 @@ def clear(self) -> None: and :meth:`is_ready` both return ``False`` along with the bot's internal cache cleared. """ - self._closed = False + self._closing_task = None self._ready.clear() self._connection.clear() self.http.clear() @@ -870,7 +914,7 @@ async def runner(): def is_closed(self) -> bool: """:class:`bool`: Indicates if the websocket connection is closed.""" - return self._closed + return self._closing_task is not None @property def activity(self) -> Optional[ActivityTypes]: @@ -1084,6 +1128,23 @@ def get_sticker(self, id: int, /) -> Optional[GuildSticker]: """ return self._connection.get_sticker(id) + def get_soundboard_sound(self, id: int, /) -> Optional[SoundboardSound]: + """Returns a soundboard sound with the given ID. + + .. versionadded:: 2.5 + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + -------- + Optional[:class:`.SoundboardSound`] + The soundboard sound or ``None`` if not found. + """ + return self._connection.get_soundboard_sound(id) + def get_all_channels(self) -> Generator[GuildChannel, None, None]: """A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'. @@ -1152,8 +1213,8 @@ async def wait_for( event: Literal['raw_app_command_permissions_update'], /, *, - check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawAppCommandPermissionsUpdateEvent: ... @@ -1163,8 +1224,8 @@ async def wait_for( event: Literal['app_command_completion'], /, *, - check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]: ... @@ -1176,8 +1237,8 @@ async def wait_for( event: Literal['automod_rule_create', 'automod_rule_update', 'automod_rule_delete'], /, *, - check: Optional[Callable[[AutoModRule], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModRule], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModRule: ... @@ -1187,8 +1248,8 @@ async def wait_for( event: Literal['automod_action'], /, *, - check: Optional[Callable[[AutoModAction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModAction], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModAction: ... @@ -1200,8 +1261,8 @@ async def wait_for( event: Literal['private_channel_update'], /, *, - check: Optional[Callable[[GroupChannel, GroupChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GroupChannel, GroupChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GroupChannel, GroupChannel]: ... @@ -1211,8 +1272,8 @@ async def wait_for( event: Literal['private_channel_pins_update'], /, *, - check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[PrivateChannel, datetime.datetime]: ... @@ -1222,8 +1283,8 @@ async def wait_for( event: Literal['guild_channel_delete', 'guild_channel_create'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1233,8 +1294,8 @@ async def wait_for( event: Literal['guild_channel_update'], /, *, - check: Optional[Callable[[GuildChannel, GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel, GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GuildChannel, GuildChannel]: ... @@ -1250,7 +1311,7 @@ async def wait_for( bool, ] ], - timeout: Optional[float] = None, + timeout: Optional[float] = ..., ) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]: ... @@ -1260,8 +1321,8 @@ async def wait_for( event: Literal['typing'], /, *, - check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Messageable, Union[User, Member], datetime.datetime]: ... @@ -1271,8 +1332,8 @@ async def wait_for( event: Literal['raw_typing'], /, *, - check: Optional[Callable[[RawTypingEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawTypingEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawTypingEvent: ... @@ -1284,8 +1345,8 @@ async def wait_for( event: Literal['connect', 'disconnect', 'ready', 'resumed'], /, *, - check: Optional[Callable[[], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[], bool]] = ..., + timeout: Optional[float] = ..., ) -> None: ... @@ -1295,8 +1356,8 @@ async def wait_for( event: Literal['shard_connect', 'shard_disconnect', 'shard_ready', 'shard_resumed'], /, *, - check: Optional[Callable[[int], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[int], bool]] = ..., + timeout: Optional[float] = ..., ) -> int: ... @@ -1306,8 +1367,8 @@ async def wait_for( event: Literal['socket_event_type', 'socket_raw_receive'], /, *, - check: Optional[Callable[[str], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[str], bool]] = ..., + timeout: Optional[float] = ..., ) -> str: ... @@ -1317,11 +1378,23 @@ async def wait_for( event: Literal['socket_raw_send'], /, *, - check: Optional[Callable[[Union[str, bytes]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Union[str, bytes]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Union[str, bytes]: ... + # Entitlements + @overload + async def wait_for( + self, + event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'], + /, + *, + check: Optional[Callable[[Entitlement], bool]] = ..., + timeout: Optional[float] = ..., + ) -> Entitlement: + ... + # Guilds @overload @@ -1335,8 +1408,8 @@ async def wait_for( ], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1346,8 +1419,8 @@ async def wait_for( event: Literal['guild_update'], /, *, - check: Optional[Callable[[Guild, Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Guild]: ... @@ -1357,8 +1430,8 @@ async def wait_for( event: Literal['guild_emojis_update'], /, *, - check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]: ... @@ -1368,8 +1441,8 @@ async def wait_for( event: Literal['guild_stickers_update'], /, *, - check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]: ... @@ -1379,8 +1452,8 @@ async def wait_for( event: Literal['invite_create', 'invite_delete'], /, *, - check: Optional[Callable[[Invite], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Invite], bool]] = ..., + timeout: Optional[float] = ..., ) -> Invite: ... @@ -1390,8 +1463,8 @@ async def wait_for( event: Literal['audit_log_entry_create'], /, *, - check: Optional[Callable[[AuditLogEntry], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AuditLogEntry], bool]] = ..., + timeout: Optional[float] = ..., ) -> AuditLogEntry: ... @@ -1403,8 +1476,8 @@ async def wait_for( event: Literal['integration_create', 'integration_update'], /, *, - check: Optional[Callable[[Integration], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Integration], bool]] = ..., + timeout: Optional[float] = ..., ) -> Integration: ... @@ -1414,8 +1487,8 @@ async def wait_for( event: Literal['guild_integrations_update'], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1425,8 +1498,8 @@ async def wait_for( event: Literal['webhooks_update'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1436,8 +1509,8 @@ async def wait_for( event: Literal['raw_integration_delete'], /, *, - check: Optional[Callable[[RawIntegrationDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawIntegrationDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawIntegrationDeleteEvent: ... @@ -1449,8 +1522,8 @@ async def wait_for( event: Literal['interaction'], /, *, - check: Optional[Callable[[Interaction[Self]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Interaction[Self]: ... @@ -1462,8 +1535,8 @@ async def wait_for( event: Literal['member_join', 'member_remove'], /, *, - check: Optional[Callable[[Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Member: ... @@ -1473,8 +1546,8 @@ async def wait_for( event: Literal['raw_member_remove'], /, *, - check: Optional[Callable[[RawMemberRemoveEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMemberRemoveEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMemberRemoveEvent: ... @@ -1484,8 +1557,8 @@ async def wait_for( event: Literal['member_update', 'presence_update'], /, *, - check: Optional[Callable[[Member, Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, Member]: ... @@ -1495,8 +1568,8 @@ async def wait_for( event: Literal['user_update'], /, *, - check: Optional[Callable[[User, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[User, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[User, User]: ... @@ -1506,8 +1579,8 @@ async def wait_for( event: Literal['member_ban'], /, *, - check: Optional[Callable[[Guild, Union[User, Member]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Union[User, Member]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Union[User, Member]]: ... @@ -1517,8 +1590,8 @@ async def wait_for( event: Literal['member_unban'], /, *, - check: Optional[Callable[[Guild, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, User]: ... @@ -1530,8 +1603,8 @@ async def wait_for( event: Literal['message', 'message_delete'], /, *, - check: Optional[Callable[[Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Message: ... @@ -1541,8 +1614,8 @@ async def wait_for( event: Literal['message_edit'], /, *, - check: Optional[Callable[[Message, Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, Message]: ... @@ -1552,8 +1625,8 @@ async def wait_for( event: Literal['bulk_message_delete'], /, *, - check: Optional[Callable[[List[Message]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[List[Message]], bool]] = ..., + timeout: Optional[float] = ..., ) -> List[Message]: ... @@ -1563,8 +1636,8 @@ async def wait_for( event: Literal['raw_message_edit'], /, *, - check: Optional[Callable[[RawMessageUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageUpdateEvent: ... @@ -1574,8 +1647,8 @@ async def wait_for( event: Literal['raw_message_delete'], /, *, - check: Optional[Callable[[RawMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageDeleteEvent: ... @@ -1585,8 +1658,8 @@ async def wait_for( event: Literal['raw_bulk_message_delete'], /, *, - check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawBulkMessageDeleteEvent: ... @@ -1598,8 +1671,8 @@ async def wait_for( event: Literal['reaction_add', 'reaction_remove'], /, *, - check: Optional[Callable[[Reaction, Union[Member, User]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction, Union[Member, User]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Reaction, Union[Member, User]]: ... @@ -1609,8 +1682,8 @@ async def wait_for( event: Literal['reaction_clear'], /, *, - check: Optional[Callable[[Message, List[Reaction]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, List[Reaction]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, List[Reaction]]: ... @@ -1620,8 +1693,8 @@ async def wait_for( event: Literal['reaction_clear_emoji'], /, *, - check: Optional[Callable[[Reaction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction], bool]] = ..., + timeout: Optional[float] = ..., ) -> Reaction: ... @@ -1631,8 +1704,8 @@ async def wait_for( event: Literal['raw_reaction_add', 'raw_reaction_remove'], /, *, - check: Optional[Callable[[RawReactionActionEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionActionEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionActionEvent: ... @@ -1642,8 +1715,8 @@ async def wait_for( event: Literal['raw_reaction_clear'], /, *, - check: Optional[Callable[[RawReactionClearEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEvent: ... @@ -1653,8 +1726,8 @@ async def wait_for( event: Literal['raw_reaction_clear_emoji'], /, *, - check: Optional[Callable[[RawReactionClearEmojiEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEmojiEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEmojiEvent: ... @@ -1666,8 +1739,8 @@ async def wait_for( event: Literal['guild_role_create', 'guild_role_delete'], /, *, - check: Optional[Callable[[Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Role: ... @@ -1677,8 +1750,8 @@ async def wait_for( event: Literal['guild_role_update'], /, *, - check: Optional[Callable[[Role, Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role, Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Role, Role]: ... @@ -1690,8 +1763,8 @@ async def wait_for( event: Literal['scheduled_event_create', 'scheduled_event_delete'], /, *, - check: Optional[Callable[[ScheduledEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> ScheduledEvent: ... @@ -1701,8 +1774,8 @@ async def wait_for( event: Literal['scheduled_event_user_add', 'scheduled_event_user_remove'], /, *, - check: Optional[Callable[[ScheduledEvent, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[ScheduledEvent, User]: ... @@ -1714,8 +1787,8 @@ async def wait_for( event: Literal['stage_instance_create', 'stage_instance_delete'], /, *, - check: Optional[Callable[[StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> StageInstance: ... @@ -1725,11 +1798,23 @@ async def wait_for( event: Literal['stage_instance_update'], /, *, - check: Optional[Callable[[StageInstance, StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance, StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]: ... + # Subscriptions + @overload + async def wait_for( + self, + event: Literal['subscription_create', 'subscription_update', 'subscription_delete'], + /, + *, + check: Optional[Callable[[Subscription], bool]] = ..., + timeout: Optional[float] = ..., + ) -> Subscription: + ... + # Threads @overload async def wait_for( @@ -1737,8 +1822,8 @@ async def wait_for( event: Literal['thread_create', 'thread_join', 'thread_remove', 'thread_delete'], /, *, - check: Optional[Callable[[Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Thread: ... @@ -1748,8 +1833,8 @@ async def wait_for( event: Literal['thread_update'], /, *, - check: Optional[Callable[[Thread, Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread, Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Thread, Thread]: ... @@ -1759,8 +1844,8 @@ async def wait_for( event: Literal['raw_thread_update'], /, *, - check: Optional[Callable[[RawThreadUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadUpdateEvent: ... @@ -1770,8 +1855,8 @@ async def wait_for( event: Literal['raw_thread_delete'], /, *, - check: Optional[Callable[[RawThreadDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadDeleteEvent: ... @@ -1781,8 +1866,8 @@ async def wait_for( event: Literal['thread_member_join', 'thread_member_remove'], /, *, - check: Optional[Callable[[ThreadMember], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ThreadMember], bool]] = ..., + timeout: Optional[float] = ..., ) -> ThreadMember: ... @@ -1792,8 +1877,8 @@ async def wait_for( event: Literal['raw_thread_member_remove'], /, *, - check: Optional[Callable[[RawThreadMembersUpdate], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadMembersUpdate], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadMembersUpdate: ... @@ -1805,11 +1890,35 @@ async def wait_for( event: Literal['voice_state_update'], /, *, - check: Optional[Callable[[Member, VoiceState, VoiceState], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, VoiceState, VoiceState], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, VoiceState, VoiceState]: ... + # Polls + + @overload + async def wait_for( + self, + event: Literal['poll_vote_add', 'poll_vote_remove'], + /, + *, + check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = ..., + timeout: Optional[float] = ..., + ) -> Tuple[Union[User, Member], PollAnswer]: + ... + + @overload + async def wait_for( + self, + event: Literal['raw_poll_vote_add', 'raw_poll_vote_remove'], + /, + *, + check: Optional[Callable[[RawPollVoteActionEvent], bool]] = ..., + timeout: Optional[float] = ..., + ) -> RawPollVoteActionEvent: + ... + # Commands @overload @@ -1818,8 +1927,8 @@ async def wait_for( event: Literal["command", "command_completion"], /, *, - check: Optional[Callable[[Context[Any]], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Context[Any]: ... @@ -1829,8 +1938,8 @@ async def wait_for( event: Literal["command_error"], /, *, - check: Optional[Callable[[Context[Any], CommandError], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any], CommandError], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Context[Any], CommandError]: ... @@ -1840,8 +1949,8 @@ async def wait_for( event: str, /, *, - check: Optional[Callable[..., bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[..., bool]] = ..., + timeout: Optional[float] = ..., ) -> Any: ... @@ -2256,6 +2365,29 @@ async def fetch_guild(self, guild_id: int, /, *, with_counts: bool = True) -> Gu data = await self.http.get_guild(guild_id, with_counts=with_counts) return Guild(data=data, state=self._connection) + async def fetch_guild_preview(self, guild_id: int) -> GuildPreview: + """|coro| + + Retrieves a preview of a :class:`.Guild` from an ID. If the guild is discoverable, + you don't have to be a member of it. + + .. versionadded:: 2.5 + + Raises + ------ + NotFound + The guild doesn't exist, or is not discoverable and you are not in it. + HTTPException + Getting the guild failed. + + Returns + -------- + :class:`.GuildPreview` + The guild preview from the ID. + """ + data = await self.http.get_guild_preview(guild_id) + return GuildPreview(data=data, state=self._connection) + async def create_guild( self, *, @@ -2701,6 +2833,7 @@ async def entitlements( user: Optional[Snowflake] = None, guild: Optional[Snowflake] = None, exclude_ended: bool = False, + exclude_deleted: bool = True, ) -> AsyncIterator[Entitlement]: """Retrieves an :term:`asynchronous iterator` of the :class:`.Entitlement` that applications has. @@ -2742,6 +2875,10 @@ async def entitlements( The guild to filter by. exclude_ended: :class:`bool` Whether to exclude ended entitlements. Defaults to ``False``. + exclude_deleted: :class:`bool` + Whether to exclude deleted entitlements. Defaults to ``True``. + + .. versionadded:: 2.5 Raises ------- @@ -2778,6 +2915,7 @@ async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Op user_id=user.id if user else None, guild_id=guild.id if guild else None, exclude_ended=exclude_ended, + exclude_deleted=exclude_deleted, ) if data: @@ -2826,7 +2964,7 @@ async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Opti data, state, limit = await strategy(retrieve, state, limit) # Terminate loop on next iteration; there's no data left after this - if len(data) < 1000: + if len(data) < 100: limit = 0 for e in data: @@ -2888,6 +3026,53 @@ async def fetch_premium_sticker_packs(self) -> List[StickerPack]: data = await self.http.list_premium_sticker_packs() return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']] + async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPack: + """|coro| + + Retrieves a premium sticker pack with the specified ID. + + .. versionadded:: 2.5 + + Parameters + ---------- + sticker_pack_id: :class:`int` + The sticker pack's ID to fetch from. + + Raises + ------- + NotFound + A sticker pack with this ID does not exist. + HTTPException + Retrieving the sticker pack failed. + + Returns + ------- + :class:`.StickerPack` + The retrieved premium sticker pack. + """ + data = await self.http.get_sticker_pack(sticker_pack_id) + return StickerPack(state=self._connection, data=data) + + async def fetch_soundboard_default_sounds(self) -> List[SoundboardDefaultSound]: + """|coro| + + Retrieves all default soundboard sounds. + + .. versionadded:: 2.5 + + Raises + ------- + HTTPException + Retrieving the default soundboard sounds failed. + + Returns + --------- + List[:class:`.SoundboardDefaultSound`] + All default soundboard sounds. + """ + data = await self.http.get_soundboard_default_sounds() + return [SoundboardDefaultSound(state=self._connection, data=sound) for sound in data] + async def create_dm(self, user: Snowflake) -> DMChannel: """|coro| @@ -2964,7 +3149,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: self._connection.remove_dynamic_items(*items) - def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: + def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None: """Registers a :class:`~discord.ui.View` for persistent listening. This method should be used for when a view is comprised of components @@ -2974,8 +3159,11 @@ def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: Parameters ------------ - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] The view to register for dispatching. + + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. message_id: Optional[:class:`int`] The message ID that the view is attached to. This is currently used to refresh the view's state during message update events. If not given @@ -2990,7 +3178,7 @@ def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: and all their components have an explicitly provided custom_id. """ - if not isinstance(view, View): + if not isinstance(view, BaseView): raise TypeError(f'expected an instance of View not {view.__class__.__name__}') if not view.is_persistent(): @@ -3002,9 +3190,103 @@ def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: self._connection.store_view(view, message_id) @property - def persistent_views(self) -> Sequence[View]: - """Sequence[:class:`.View`]: A sequence of persistent views added to the client. + def persistent_views(self) -> Sequence[BaseView]: + """Sequence[Union[:class:`.View`, :class:`.LayoutView`]]: A sequence of persistent views added to the client. .. versionadded:: 2.0 """ return self._connection.persistent_views + + async def create_application_emoji( + self, + *, + name: str, + image: bytes, + ) -> Emoji: + """|coro| + + Create an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The emoji name. Must be between 2 and 32 characters long. + image: :class:`bytes` + The :term:`py:bytes-like object` representing the image data to use. + Only JPG, PNG and GIF images are supported. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Creating the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji that was created. + """ + if self.application_id is None: + raise MissingApplicationID + + img = utils._bytes_to_base64_data(image) + data = await self.http.create_application_emoji(self.application_id, name, img) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emoji(self, emoji_id: int, /) -> Emoji: + """|coro| + + Retrieves an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + emoji_id: :class:`int` + The emoji ID to retrieve. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji requested. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emoji(self.application_id, emoji_id) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emojis(self) -> List[Emoji]: + """|coro| + + Retrieves all emojis for the current application. + + .. versionadded:: 2.5 + + Raises + ------- + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emojis failed. + + Returns + ------- + List[:class:`.Emoji`] + The list of emojis for the current application. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emojis(self.application_id) + return [Emoji(guild=Object(0), state=self._connection, data=emoji) for emoji in data['items']] diff --git a/discord/colour.py b/discord/colour.py index 5772b145d426..7e3a37132a11 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -175,7 +175,7 @@ def from_hsv(cls, h: float, s: float, v: float) -> Self: return cls.from_rgb(*(int(x * 255) for x in rgb)) @classmethod - def from_str(cls, value: str) -> Self: + def from_str(cls, value: str) -> Colour: """Constructs a :class:`Colour` from a string. The following formats are accepted: diff --git a/discord/components.py b/discord/components.py index dca22e8bec65..22c3d714cb54 100644 --- a/discord/components.py +++ b/discord/components.py @@ -24,8 +24,29 @@ from __future__ import annotations -from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload -from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType +from typing import ( + ClassVar, + List, + Literal, + Optional, + TYPE_CHECKING, + Tuple, + Union, +) + +from .asset import AssetMixin +from .enums import ( + try_enum, + ComponentType, + ButtonStyle, + TextStyle, + ChannelType, + SelectDefaultValueType, + SeparatorSize, + MediaItemLoadingState, +) +from .flags import AttachmentFlags +from .colour import Colour from .utils import get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag @@ -33,19 +54,42 @@ from typing_extensions import Self from .types.components import ( + ComponentBase as ComponentBasePayload, Component as ComponentPayload, ButtonComponent as ButtonComponentPayload, SelectMenu as SelectMenuPayload, SelectOption as SelectOptionPayload, ActionRow as ActionRowPayload, TextInput as TextInputPayload, - ActionRowChildComponent as ActionRowChildComponentPayload, SelectDefaultValues as SelectDefaultValuesPayload, + SectionComponent as SectionComponentPayload, + TextComponent as TextComponentPayload, + MediaGalleryComponent as MediaGalleryComponentPayload, + FileComponent as FileComponentPayload, + SeparatorComponent as SeparatorComponentPayload, + MediaGalleryItem as MediaGalleryItemPayload, + ThumbnailComponent as ThumbnailComponentPayload, + ContainerComponent as ContainerComponentPayload, + UnfurledMediaItem as UnfurledMediaItemPayload, ) + from .emoji import Emoji from .abc import Snowflake + from .state import ConnectionState ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput'] + SectionComponentType = Union['TextDisplay', 'Button'] + MessageComponentType = Union[ + ActionRowChildComponentType, + SectionComponentType, + 'ActionRow', + 'SectionComponent', + 'ThumbnailComponent', + 'MediaGalleryComponent', + 'FileComponent', + 'SectionComponent', + 'Component', + ] __all__ = ( @@ -56,18 +100,35 @@ 'SelectOption', 'TextInput', 'SelectDefaultValue', + 'SectionComponent', + 'ThumbnailComponent', + 'UnfurledMediaItem', + 'MediaGalleryItem', + 'MediaGalleryComponent', + 'FileComponent', + 'SectionComponent', + 'Container', + 'TextDisplay', + 'SeparatorComponent', ) class Component: """Represents a Discord Bot UI Kit Component. - Currently, the only components supported by Discord are: + The components supported by Discord are: - :class:`ActionRow` - :class:`Button` - :class:`SelectMenu` - :class:`TextInput` + - :class:`SectionComponent` + - :class:`TextDisplay` + - :class:`ThumbnailComponent` + - :class:`MediaGalleryComponent` + - :class:`FileComponent` + - :class:`SeparatorComponent` + - :class:`Container` This class is abstract and cannot be instantiated. @@ -99,7 +160,7 @@ def _raw_construct(cls, **kwargs) -> Self: setattr(self, slot, value) return self - def to_dict(self) -> ComponentPayload: + def to_dict(self) -> ComponentBasePayload: raise NotImplementedError @@ -116,20 +177,25 @@ class ActionRow(Component): ------------ children: List[Union[:class:`Button`, :class:`SelectMenu`, :class:`TextInput`]] The children components that this holds, if any. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ - __slots__: Tuple[str, ...] = ('children',) + __slots__: Tuple[str, ...] = ('children', 'id') __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ActionRowPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.children: List[ActionRowChildComponentType] = [] for component_data in data.get('components', []): component = _component_factory(component_data) if component is not None: - self.children.append(component) + self.children.append(component) # type: ignore # should be the correct type here @property def type(self) -> Literal[ComponentType.action_row]: @@ -137,10 +203,13 @@ def type(self) -> Literal[ComponentType.action_row]: return ComponentType.action_row def to_dict(self) -> ActionRowPayload: - return { + payload: ActionRowPayload = { 'type': self.type.value, 'components': [child.to_dict() for child in self.children], } + if self.id is not None: + payload['id'] = self.id + return payload class Button(Component): @@ -170,6 +239,14 @@ class Button(Component): The label of the button, if any. emoji: Optional[:class:`PartialEmoji`] The emoji of the button, if available. + sku_id: Optional[:class:`int`] + The SKU ID this button sends you to, if available. + + .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -179,11 +256,14 @@ class Button(Component): 'disabled', 'label', 'emoji', + 'sku_id', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ButtonComponentPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.style: ButtonStyle = try_enum(ButtonStyle, data['style']) self.custom_id: Optional[str] = data.get('custom_id') self.url: Optional[str] = data.get('url') @@ -191,10 +271,15 @@ def __init__(self, data: ButtonComponentPayload, /) -> None: self.label: Optional[str] = data.get('label') self.emoji: Optional[PartialEmoji] try: - self.emoji = PartialEmoji.from_dict(data['emoji']) + self.emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.emoji = None + try: + self.sku_id: Optional[int] = int(data['sku_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] + except KeyError: + self.sku_id = None + @property def type(self) -> Literal[ComponentType.button]: """:class:`ComponentType`: The type of component.""" @@ -207,6 +292,12 @@ def to_dict(self) -> ButtonComponentPayload: 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id + + if self.sku_id: + payload['sku_id'] = str(self.sku_id) + if self.label: payload['label'] = self.label @@ -255,6 +346,10 @@ class SelectMenu(Component): Whether the select is disabled or not. channel_types: List[:class:`.ChannelType`] A list of channel types that are allowed to be chosen in this select menu. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -267,6 +362,7 @@ class SelectMenu(Component): 'disabled', 'channel_types', 'default_values', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -283,6 +379,7 @@ def __init__(self, data: SelectMenuPayload, /) -> None: self.default_values: List[SelectDefaultValue] = [ SelectDefaultValue.from_dict(d) for d in data.get('default_values', []) ] + self.id: Optional[int] = data.get('id') def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { @@ -292,6 +389,8 @@ def to_dict(self) -> SelectMenuPayload: 'max_values': self.max_values, 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id if self.placeholder: payload['placeholder'] = self.placeholder if self.options: @@ -299,7 +398,7 @@ def to_dict(self) -> SelectMenuPayload: if self.channel_types: payload['channel_types'] = [t.value for t in self.channel_types] if self.default_values: - payload["default_values"] = [v.to_dict() for v in self.default_values] + payload['default_values'] = [v.to_dict() for v in self.default_values] return payload @@ -318,8 +417,8 @@ class SelectOption: Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. - If not provided when constructed then it defaults to the - label. Can only be up to 100 characters. + If not provided when constructed then it defaults to the label. + Can only be up to 100 characters. description: Optional[:class:`str`] An additional description of the option, if any. Can only be up to 100 characters. @@ -332,14 +431,12 @@ class SelectOption: ----------- label: :class:`str` The label of the option. This is displayed to users. - Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. If not provided when constructed then it defaults to the - label. Can only be up to 100 characters. + label. description: Optional[:class:`str`] An additional description of the option, if any. - Can only be up to 100 characters. default: :class:`bool` Whether this option is selected by default. """ @@ -404,7 +501,7 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: @classmethod def from_dict(cls, data: SelectOptionPayload) -> SelectOption: try: - emoji = PartialEmoji.from_dict(data['emoji']) + emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: emoji = None @@ -459,6 +556,10 @@ class TextInput(Component): The minimum length of the text input. max_length: Optional[:class:`int`] The maximum length of the text input. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -470,6 +571,7 @@ class TextInput(Component): 'required', 'min_length', 'max_length', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -483,6 +585,7 @@ def __init__(self, data: TextInputPayload, /) -> None: self.required: bool = data.get('required', True) self.min_length: Optional[int] = data.get('min_length') self.max_length: Optional[int] = data.get('max_length') + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.text_input]: @@ -498,6 +601,9 @@ def to_dict(self) -> TextInputPayload: 'required': self.required, } + if self.id is not None: + payload['id'] = self.id + if self.placeholder: payload['placeholder'] = self.placeholder @@ -631,17 +737,536 @@ def from_user(cls, user: Snowflake, /) -> Self: ) -@overload -def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]: - ... +class SectionComponent(Component): + """Represents a section from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type to create a section is :class:`discord.ui.Section` + not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + components: List[Union[:class:`TextDisplay`, :class:`Button`]] + The components on this section. + accessory: :class:`Component` + The section accessory. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ( + 'components', + 'accessory', + 'id', + ) + + __repr_info__ = __slots__ + + def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: + self.components: List[SectionComponentType] = [] + self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore + self.id: Optional[int] = data.get('id') + + for component_data in data['components']: + component = _component_factory(component_data, state) + if component is not None: + self.components.append(component) # type: ignore # should be the correct type here + + @property + def type(self) -> Literal[ComponentType.section]: + return ComponentType.section + + def to_dict(self) -> SectionComponentPayload: + payload: SectionComponentPayload = { + 'type': self.type.value, + 'components': [c.to_dict() for c in self.components], + 'accessory': self.accessory.to_dict(), + } + + if self.id is not None: + payload['id'] = self.id + + return payload + + +class ThumbnailComponent(Component): + """Represents a Thumbnail from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type to create a thumbnail is :class:`discord.ui.Thumbnail` + not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The media for this thumbnail. + description: Optional[:class:`str`] + The description shown within this thumbnail. + spoiler: :class:`bool` + Whether this thumbnail is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ( + 'media', + 'spoiler', + 'description', + 'id', + ) + + __repr_info__ = __slots__ + + def __init__( + self, + data: ThumbnailComponentPayload, + state: Optional[ConnectionState], + ) -> None: + self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state) + self.description: Optional[str] = data.get('description') + self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') + + @property + def type(self) -> Literal[ComponentType.thumbnail]: + return ComponentType.thumbnail + + def to_dict(self) -> ThumbnailComponentPayload: + payload = { + 'media': self.media.to_dict(), + 'description': self.description, + 'spoiler': self.spoiler, + 'type': self.type.value, + } + + if self.id is not None: + payload['id'] = self.id + + return payload # type: ignore + + +class TextDisplay(Component): + """Represents a text display from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type to create a text display is + :class:`discord.ui.TextDisplay` not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + content: :class:`str` + The content that this display shows. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ('content', 'id') + + __repr_info__ = __slots__ + + def __init__(self, data: TextComponentPayload) -> None: + self.content: str = data['content'] + self.id: Optional[int] = data.get('id') + + @property + def type(self) -> Literal[ComponentType.text_display]: + return ComponentType.text_display + + def to_dict(self) -> TextComponentPayload: + payload: TextComponentPayload = { + 'type': self.type.value, + 'content': self.content, + } + if self.id is not None: + payload['id'] = self.id + return payload + + +class UnfurledMediaItem(AssetMixin): + """Represents an unfurled media item. + + .. versionadded:: 2.6 + + Parameters + ---------- + url: :class:`str` + The URL of this media item. + + Attributes + ---------- + url: :class:`str` + The URL of this media item. + proxy_url: Optional[:class:`str`] + The proxy URL. This is a cached version of the :attr:`.url` in the + case of images. When the message is deleted, this URL might be valid for a few minutes + or not valid at all. + height: Optional[:class:`int`] + The media item's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The media item's width, in pixels. Only applicable to images and videos. + content_type: Optional[:class:`str`] + The media item's `media type `_ + placeholder: Optional[:class:`str`] + The media item's placeholder. + loading_state: Optional[:class:`MediaItemLoadingState`] + The loading state of this media item. + """ + + __slots__ = ( + 'url', + 'proxy_url', + 'height', + 'width', + 'content_type', + '_flags', + 'placeholder', + 'loading_state', + '_state', + ) + + def __init__(self, url: str) -> None: + self.url: str = url + + self.proxy_url: Optional[str] = None + self.height: Optional[int] = None + self.width: Optional[int] = None + self.content_type: Optional[str] = None + self._flags: int = 0 + self.placeholder: Optional[str] = None + self.loading_state: Optional[MediaItemLoadingState] = None + self._state: Optional[ConnectionState] = None + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: This media item's flags.""" + return AttachmentFlags._from_value(self._flags) + + @classmethod + def _from_data(cls, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]): + self = cls(data['url']) + self._update(data, state) + return self + + def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]) -> None: + self.proxy_url = data['proxy_url'] + self.height = data.get('height') + self.width = data.get('width') + self.content_type = data.get('content_type') + self._flags = data.get('flags', 0) + self.placeholder = data.get('placeholder') + self.loading_state = try_enum(MediaItemLoadingState, data['loading_state']) + self._state = state + + def __repr__(self) -> str: + return f'' + + def to_dict(self): + return { + 'url': self.url, + } -@overload -def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: - ... +class MediaGalleryItem: + """Represents a :class:`MediaGalleryComponent` media item. + + .. versionadded:: 2.6 + + Parameters + ---------- + media: Union[:class:`str`, :class:`UnfurledMediaItem`] + The media item data. This can be a string representing a local + file uploaded as an attachment in the message, that can be accessed + using the ``attachment://file-name.extension`` format. + description: Optional[:class:`str`] + The description to show within this item. Up to 256 characters. Defaults + to ``None``. + spoiler: :class:`bool` + Whether this item should be flagged as a spoiler. + """ + + __slots__ = ( + 'media', + 'description', + 'spoiler', + '_state', + ) + + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + description: Optional[str] = None, + spoiler: bool = False, + ) -> None: + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media + self.description: Optional[str] = description + self.spoiler: bool = spoiler + self._state: Optional[ConnectionState] = None + + def __repr__(self) -> str: + return f'' + + @classmethod + def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem: + media = data['media'] + self = cls( + media=UnfurledMediaItem._from_data(media, state), + description=data.get('description'), + spoiler=data.get('spoiler', False), + ) + self._state = state + return self + + @classmethod + def _from_gallery( + cls, + items: List[MediaGalleryItemPayload], + state: Optional[ConnectionState], + ) -> List[MediaGalleryItem]: + return [cls._from_data(item, state) for item in items] + + def to_dict(self) -> MediaGalleryItemPayload: + return { + 'media': self.media.to_dict(), # type: ignore + 'description': self.description, + 'spoiler': self.spoiler, + } + + +class MediaGalleryComponent(Component): + """Represents a Media Gallery component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type for creating a media gallery is + :class:`discord.ui.MediaGallery` not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The items this gallery has. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ('items', 'id') + + __repr_info__ = __slots__ + + def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None: + self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state) + self.id: Optional[int] = data.get('id') + + @property + def type(self) -> Literal[ComponentType.media_gallery]: + return ComponentType.media_gallery + + def to_dict(self) -> MediaGalleryComponentPayload: + payload: MediaGalleryComponentPayload = { + 'type': self.type.value, + 'items': [item.to_dict() for item in self.items], + } + if self.id is not None: + payload['id'] = self.id + return payload + + +class FileComponent(Component): + """Represents a File component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type for create a file component is + :class:`discord.ui.File` not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The unfurled attachment contents of the file. + spoiler: :class:`bool` + Whether this file is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ( + 'media', + 'spoiler', + 'id', + ) + + __repr_info__ = __slots__ + + def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: + self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state) + self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') + + @property + def type(self) -> Literal[ComponentType.file]: + return ComponentType.file + + def to_dict(self) -> FileComponentPayload: + payload: FileComponentPayload = { + 'type': self.type.value, + 'file': self.media.to_dict(), # type: ignore + 'spoiler': self.spoiler, + } + if self.id is not None: + payload['id'] = self.id + return payload + + +class SeparatorComponent(Component): + """Represents a Separator from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type for creating a separator is + :class:`discord.ui.Separator` not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + spacing: :class:`SeparatorSize` + The spacing size of the separator. + visible: :class:`bool` + Whether this separator is visible and shows a divider. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ( + 'spacing', + 'visible', + 'id', + ) + + __repr_info__ = __slots__ + + def __init__( + self, + data: SeparatorComponentPayload, + ) -> None: + self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1)) + self.visible: bool = data.get('divider', True) + self.id: Optional[int] = data.get('id') + + @property + def type(self) -> Literal[ComponentType.separator]: + return ComponentType.separator + + def to_dict(self) -> SeparatorComponentPayload: + payload: SeparatorComponentPayload = { + 'type': self.type.value, + 'divider': self.visible, + 'spacing': self.spacing.value, + } + if self.id is not None: + payload['id'] = self.id + return payload + + +class Container(Component): + """Represents a Container from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type for creating a container is + :class:`discord.ui.Container` not this one. + + .. versionadded:: 2.6 + + Attributes + ---------- + children: :class:`Component` + This container's children. + spoiler: :class:`bool` + Whether this container is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. + """ + + __slots__ = ( + 'children', + 'id', + 'spoiler', + '_colour', + ) + + __repr_info__ = ( + 'children', + 'id', + 'spoiler', + 'accent_colour', + ) + + def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None: + self.children: List[Component] = [] + self.id: Optional[int] = data.get('id') + + for child in data['components']: + comp = _component_factory(child, state) + + if comp: + self.children.append(comp) + + self.spoiler: bool = data.get('spoiler', False) + + colour = data.get('accent_color') + self._colour: Optional[Colour] = None + if colour is not None: + self._colour = Colour(colour) + + @property + def accent_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The container's accent colour.""" + return self._colour + + accent_color = accent_colour + + def to_dict(self) -> ContainerComponentPayload: + payload: ContainerComponentPayload = { + 'type': self.type.value, # type: ignore + 'spoiler': self.spoiler, + 'components': [c.to_dict() for c in self.children], + } + if self.id is not None: + payload['id'] = self.id + if self._colour: + payload['accent_color'] = self._colour.value + return payload -def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]: if data['type'] == 1: return ActionRow(data) elif data['type'] == 2: @@ -649,4 +1274,18 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti elif data['type'] == 4: return TextInput(data) elif data['type'] in (3, 5, 6, 7, 8): - return SelectMenu(data) + return SelectMenu(data) # type: ignore + elif data['type'] == 9: + return SectionComponent(data, state) + elif data['type'] == 10: + return TextDisplay(data) + elif data['type'] == 11: + return ThumbnailComponent(data, state) + elif data['type'] == 12: + return MediaGalleryComponent(data, state) + elif data['type'] == 13: + return FileComponent(data, state) + elif data['type'] == 14: + return SeparatorComponent(data) + elif data['type'] == 17: + return Container(data, state) diff --git a/discord/embeds.py b/discord/embeds.py index 6a79fef71c62..7f84e410d341 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -29,6 +29,7 @@ from . import utils from .colour import Colour +from .flags import AttachmentFlags, EmbedFlags # fmt: off __all__ = ( @@ -45,7 +46,7 @@ def __len__(self) -> int: return len(self.__dict__) def __repr__(self) -> str: - inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_'))) + inner = ', '.join((f'{k}={getattr(self, k)!r}' for k in dir(self) if not k.startswith('_'))) return f'EmbedProxy({inner})' def __getattr__(self, attr: str) -> None: @@ -55,6 +56,16 @@ def __eq__(self, other: object) -> bool: return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__ +class EmbedMediaProxy(EmbedProxy): + def __init__(self, layer: Dict[str, Any]): + super().__init__(layer) + self._flags = self.__dict__.pop('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + return AttachmentFlags._from_value(self._flags or 0) + + if TYPE_CHECKING: from typing_extensions import Self @@ -76,11 +87,7 @@ class _EmbedMediaProxy(Protocol): proxy_url: Optional[str] height: Optional[int] width: Optional[int] - - class _EmbedVideoProxy(Protocol): - url: Optional[str] - height: Optional[int] - width: Optional[int] + flags: AttachmentFlags class _EmbedProviderProxy(Protocol): name: Optional[str] @@ -131,7 +138,7 @@ class Embed: The type of embed. Usually "rich". This can be set during initialisation. Possible strings for embed types can be found on discord's - :ddocs:`api docs ` + :ddocs:`api docs ` description: Optional[:class:`str`] The description of the embed. This can be set during initialisation. @@ -162,6 +169,7 @@ class Embed: '_author', '_fields', 'description', + '_flags', ) def __init__( @@ -181,6 +189,7 @@ def __init__( self.type: EmbedType = type self.url: Optional[str] = url self.description: Optional[str] = description + self._flags: int = 0 if self.title is not None: self.title = str(self.title) @@ -199,7 +208,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: """Converts a :class:`dict` to a :class:`Embed` provided it is in the format that Discord expects it to be in. - You can find out about this format in the :ddocs:`official Discord documentation `. + You can find out about this format in the :ddocs:`official Discord documentation `. Parameters ----------- @@ -215,6 +224,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: self.type = data.get('type', None) self.description = data.get('description', None) self.url = data.get('url', None) + self._flags = data.get('flags', 0) if self.title is not None: self.title = str(self.title) @@ -305,8 +315,17 @@ def __eq__(self, other: Embed) -> bool: and self.image == other.image and self.provider == other.provider and self.video == other.video + and self._flags == other._flags ) + @property + def flags(self) -> EmbedFlags: + """:class:`EmbedFlags`: The flags of this embed. + + .. versionadded:: 2.5 + """ + return EmbedFlags._from_value(self._flags or 0) + @property def colour(self) -> Optional[Colour]: return getattr(self, '_colour', None) @@ -395,15 +414,16 @@ def image(self) -> _EmbedMediaProxy: Possible attributes you can access are: - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` + - ``url`` for the image URL. + - ``proxy_url`` for the proxied image URL. + - ``width`` for the image width. + - ``height`` for the image height. + - ``flags`` for the image's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_image', {})) # type: ignore + return EmbedMediaProxy(getattr(self, '_image', {})) # type: ignore def set_image(self, *, url: Optional[Any]) -> Self: """Sets the image for the embed content. @@ -413,8 +433,9 @@ def set_image(self, *, url: Optional[Any]) -> Self: Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the image. Only HTTP(S) is supported. + If ``None`` is passed, any existing image is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ @@ -436,15 +457,16 @@ def thumbnail(self) -> _EmbedMediaProxy: Possible attributes you can access are: - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` + - ``url`` for the thumbnail URL. + - ``proxy_url`` for the proxied thumbnail URL. + - ``width`` for the thumbnail width. + - ``height`` for the thumbnail height. + - ``flags`` for the thumbnail's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore + return EmbedMediaProxy(getattr(self, '_thumbnail', {})) # type: ignore def set_thumbnail(self, *, url: Optional[Any]) -> Self: """Sets the thumbnail for the embed content. @@ -452,13 +474,11 @@ def set_thumbnail(self, *, url: Optional[Any]) -> Self: This function returns the class instance to allow for fluent-style chaining. - .. versionchanged:: 1.4 - Passing ``None`` removes the thumbnail. - Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the thumbnail. Only HTTP(S) is supported. + If ``None`` is passed, any existing thumbnail is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ @@ -475,19 +495,21 @@ def set_thumbnail(self, *, url: Optional[Any]) -> Self: return self @property - def video(self) -> _EmbedVideoProxy: + def video(self) -> _EmbedMediaProxy: """Returns an ``EmbedProxy`` denoting the video contents. Possible attributes include: - ``url`` for the video URL. + - ``proxy_url`` for the proxied video URL. - ``height`` for the video height. - ``width`` for the video width. + - ``flags`` for the video's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_video', {})) # type: ignore + return EmbedMediaProxy(getattr(self, '_video', {})) # type: ignore @property def provider(self) -> _EmbedProviderProxy: diff --git a/discord/emoji.py b/discord/emoji.py index 045486d5a8a3..74f344acc1d4 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -29,6 +29,8 @@ from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag, PartialEmoji from .user import User +from .errors import MissingApplicationID +from .object import Object # fmt: off __all__ = ( @@ -93,6 +95,10 @@ class Emoji(_EmojiTag, AssetMixin): user: Optional[:class:`User`] The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and having :attr:`~Permissions.manage_emojis`. + + Or if :meth:`.is_application_owned` is ``True``, this is the team member that uploaded + the emoji, or the bot user if it was uploaded using the API and this can + only be retrieved using :meth:`~discord.Client.fetch_application_emoji` or :meth:`~discord.Client.fetch_application_emojis`. """ __slots__: Tuple[str, ...] = ( @@ -108,7 +114,7 @@ class Emoji(_EmojiTag, AssetMixin): 'available', ) - def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload) -> None: + def __init__(self, *, guild: Snowflake, state: ConnectionState, data: EmojiPayload) -> None: self.guild_id: int = guild.id self._state: ConnectionState = state self._from_data(data) @@ -196,20 +202,32 @@ async def delete(self, *, reason: Optional[str] = None) -> None: Deletes the custom emoji. - You must have :attr:`~Permissions.manage_emojis` to do this. + You must have :attr:`~Permissions.manage_emojis` to do this if + :meth:`.is_application_owned` is ``False``. Parameters ----------- reason: Optional[:class:`str`] The reason for deleting this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to delete emojis. HTTPException An error occurred deleting the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing. """ + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + await self._state.http.delete_application_emoji(application_id, self.id) + return await self._state.http.delete_custom_emoji(self.guild_id, self.id, reason=reason) @@ -231,15 +249,22 @@ async def edit( The new emoji name. roles: List[:class:`~discord.abc.Snowflake`] A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. + + This does not apply if :meth:`.is_application_owned` is ``True``. + reason: Optional[:class:`str`] The reason for editing this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to edit emojis. HTTPException An error occurred editing the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing Returns -------- @@ -253,5 +278,25 @@ async def edit( if roles is not MISSING: payload['roles'] = [role.id for role in roles] + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + payload.pop('roles', None) + data = await self._state.http.edit_application_emoji( + application_id, + self.id, + payload=payload, + ) + return Emoji(guild=Object(0), data=data, state=self._state) + data = await self._state.http.edit_custom_emoji(self.guild_id, self.id, payload=payload, reason=reason) return Emoji(guild=self.guild, data=data, state=self._state) # type: ignore # if guild is None, the http request would have failed + + def is_application_owned(self) -> bool: + """:class:`bool`: Whether the emoji is owned by an application. + + .. versionadded:: 2.5 + """ + return self.guild_id == 0 diff --git a/discord/enums.py b/discord/enums.py index ed498b8be190..49684935f7c7 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -73,23 +73,26 @@ 'SKUType', 'EntitlementType', 'EntitlementOwnerType', + 'PollLayoutType', + 'VoiceChannelEffectAnimationType', + 'SubscriptionStatus', + 'MessageReferenceType', + 'SeparatorSize', + 'MediaItemLoadingState', ) -if TYPE_CHECKING: - from typing_extensions import Self - def _create_value_cls(name: str, comparable: bool): # All the type ignores here are due to the type checker being unable to recognise # Runtime type creation without exploding. cls = namedtuple('_EnumValue_' + name, 'name value') - cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' # type: ignore - cls.__str__ = lambda self: f'{name}.{self.name}' # type: ignore + cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' + cls.__str__ = lambda self: f'{name}.{self.name}' if comparable: - cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value # type: ignore - cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value # type: ignore - cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value # type: ignore - cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value # type: ignore + cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value + cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value + cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value + cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value return cls @@ -104,7 +107,14 @@ class EnumMeta(type): _enum_member_map_: ClassVar[Dict[str, Any]] _enum_value_map_: ClassVar[Dict[Any, Any]] - def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any], *, comparable: bool = False) -> Self: + def __new__( + cls, + name: str, + bases: Tuple[type, ...], + attrs: Dict[str, Any], + *, + comparable: bool = False, + ) -> EnumMeta: value_mapping = {} member_mapping = {} member_names = [] @@ -213,6 +223,12 @@ def __str__(self) -> str: return self.name +class MessageReferenceType(Enum): + default = 0 + reply = 0 + forward = 1 + + class MessageType(Enum): default = 0 recipient_add = 1 @@ -251,6 +267,8 @@ class MessageType(Enum): guild_incident_alert_mode_disabled = 37 guild_incident_report_raid = 38 guild_incident_report_false_alarm = 39 + purchase_notification = 44 + poll_result = 46 class SpeakingState(Enum): @@ -372,6 +390,9 @@ class AuditLogAction(Enum): thread_update = 111 thread_delete = 112 app_command_permission_update = 121 + soundboard_sound_create = 130 + soundboard_sound_update = 131 + soundboard_sound_delete = 132 automod_rule_create = 140 automod_rule_update = 141 automod_rule_delete = 142 @@ -442,6 +463,9 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.automod_timeout_member: None, AuditLogAction.creator_monetization_request_created: None, AuditLogAction.creator_monetization_terms_accepted: None, + AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create, + AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update, + AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete, } # fmt: on return lookup[self] @@ -598,7 +622,7 @@ class InteractionResponseType(Enum): message_update = 7 # for components autocomplete_result = 8 modal = 9 # for modals - premium_required = 10 + # premium_required = 10 (deprecated) class VideoQualityMode(Enum): @@ -619,6 +643,13 @@ class ComponentType(Enum): role_select = 6 mentionable_select = 7 channel_select = 8 + section = 9 + text_display = 10 + thumbnail = 11 + media_gallery = 12 + file = 13 + separator = 14 + container = 17 def __int__(self) -> int: return self.value @@ -630,6 +661,7 @@ class ButtonStyle(Enum): success = 3 danger = 4 link = 5 + premium = 6 # Aliases blurple = 1 @@ -792,11 +824,20 @@ class SelectDefaultValueType(Enum): class SKUType(Enum): + durable = 2 + consumable = 3 subscription = 5 subscription_group = 6 class EntitlementType(Enum): + purchase = 1 + premium_subscription = 2 + developer_gift = 3 + test_mode_purchase = 4 + free_purchase = 5 + user_gift = 6 + premium_purchase = 7 application_subscription = 8 @@ -805,6 +846,44 @@ class EntitlementOwnerType(Enum): user = 2 +class PollLayoutType(Enum): + default = 1 + + +class InviteType(Enum): + guild = 0 + group_dm = 1 + friend = 2 + + +class ReactionType(Enum): + normal = 0 + burst = 1 + + +class VoiceChannelEffectAnimationType(Enum): + premium = 0 + basic = 1 + + +class SubscriptionStatus(Enum): + active = 0 + ending = 1 + inactive = 2 + + +class SeparatorSize(Enum): + small = 1 + large = 2 + + +class MediaItemLoadingState(Enum): + unknown = 0 + loading = 1 + loaded = 2 + not_found = 3 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/errors.py b/discord/errors.py index 6035ace7c20f..a40842578d0d 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -47,6 +47,12 @@ 'ConnectionClosed', 'PrivilegedIntentsRequired', 'InteractionResponded', + 'MissingApplicationID', +) + +APP_ID_NOT_FOUND = ( + 'Client does not have an application_id set. Either the function was called before on_ready ' + 'was called or application_id was not passed to the Client constructor.' ) @@ -278,3 +284,22 @@ class InteractionResponded(ClientException): def __init__(self, interaction: Interaction): self.interaction: Interaction = interaction super().__init__('This interaction has already been responded to before') + + +class MissingApplicationID(ClientException): + """An exception raised when the client does not have an application ID set. + + An application ID is required for syncing application commands and various + other application tasks such as SKUs or application emojis. + + This inherits from :exc:`~discord.app_commands.AppCommandError` + and :class:`~discord.ClientException`. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.5 + This is now exported to the ``discord`` namespace and now inherits from :class:`~discord.ClientException`. + """ + + def __init__(self, message: Optional[str] = None): + super().__init__(message or APP_ID_NOT_FOUND) diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index b691c5af29a9..8ce872f1af7a 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -166,14 +166,21 @@ def __init__( help_command: Optional[HelpCommand] = _default, tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree, description: Optional[str] = None, + allowed_contexts: app_commands.AppCommandContext = MISSING, + allowed_installs: app_commands.AppInstallationType = MISSING, intents: discord.Intents, **options: Any, ) -> None: super().__init__(intents=intents, **options) - self.command_prefix: PrefixType[BotT] = command_prefix + self.command_prefix: PrefixType[BotT] = command_prefix # type: ignore self.extra_events: Dict[str, List[CoroFunc]] = {} # Self doesn't have the ClientT bound, but since this is a mixin it technically does self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore + if allowed_contexts is not MISSING: + self.__tree.allowed_contexts = allowed_contexts + if allowed_installs is not MISSING: + self.__tree.allowed_installs = allowed_installs + self.__cogs: Dict[str, Cog] = {} self.__extensions: Dict[str, types.ModuleType] = {} self._checks: List[UserCheck] = [] @@ -480,7 +487,7 @@ async def can_run(self, ctx: Context[BotT], /, *, call_once: bool = False) -> bo if len(data) == 0: return True - return await discord.utils.async_all(f(ctx) for f in data) + return await discord.utils.async_all(f(ctx) for f in data) # type: ignore async def is_owner(self, user: User, /) -> bool: """|coro| @@ -521,7 +528,6 @@ async def is_owner(self, user: User, /) -> bool: elif self.owner_ids: return user.id in self.owner_ids else: - app: discord.AppInfo = await self.application_info() # type: ignore if app.team: self.owner_ids = ids = { @@ -1489,6 +1495,20 @@ class Bot(BotBase, discord.Client): The type of application command tree to use. Defaults to :class:`~discord.app_commands.CommandTree`. .. versionadded:: 2.0 + allowed_contexts: :class:`~discord.app_commands.AppCommandContext` + The default allowed contexts that applies to all application commands + in the application command tree. + + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 + allowed_installs: :class:`~discord.app_commands.AppInstallationType` + The default allowed install locations that apply to all application commands + in the application command tree. + + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 """ pass diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 1ffe25c702bb..659d69ebb433 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -169,7 +169,7 @@ async def bar(self, ctx): __cog_app_commands__: List[Union[app_commands.Group, app_commands.Command[Any, ..., Any]]] __cog_listeners__: List[Tuple[str, str]] - def __new__(cls, *args: Any, **kwargs: Any) -> Self: + def __new__(cls, *args: Any, **kwargs: Any) -> CogMeta: name, bases, attrs = args if any(issubclass(base, app_commands.Group) for base in bases): raise TypeError( @@ -318,6 +318,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: parent=None, guild_ids=getattr(cls, '__discord_app_commands_default_guilds__', None), guild_only=getattr(cls, '__discord_app_commands_guild_only__', False), + allowed_contexts=getattr(cls, '__discord_app_commands_contexts__', None), + allowed_installs=getattr(cls, '__discord_app_commands_installation_types__', None), default_permissions=getattr(cls, '__discord_app_commands_default_permissions__', None), extras=cls.__cog_group_extras__, ) @@ -366,7 +368,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: child.wrapped = lookup[child.qualified_name] # type: ignore if self.__cog_app_commands_group__: - children.append(app_command) # type: ignore # Somehow it thinks it can be None here + children.append(app_command) if Cog._get_overridden_method(self.cog_app_command_error) is not None: error_handler = self.cog_app_command_error diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 5fc675acdb34..3a7204e9b7bc 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -48,8 +48,9 @@ from discord.mentions import AllowedMentions from discord.sticker import GuildSticker, StickerItem from discord.message import MessageReference, PartialMessage - from discord.ui import View + from discord.ui.view import BaseView, View, LayoutView from discord.types.interactions import ApplicationCommandInteractionData + from discord.poll import Poll from .cog import Cog from .core import Command @@ -81,16 +82,20 @@ def is_cog(obj: Any) -> TypeGuard[Cog]: return hasattr(obj, '__cog_commands__') -class DeferTyping: +class DeferTyping(Generic[BotT]): def __init__(self, ctx: Context[BotT], *, ephemeral: bool): self.ctx: Context[BotT] = ctx self.ephemeral: bool = ephemeral + async def do_defer(self) -> None: + if self.ctx.interaction and not self.ctx.interaction.response.is_done(): + await self.ctx.interaction.response.defer(ephemeral=self.ephemeral) + def __await__(self) -> Generator[Any, None, None]: - return self.ctx.defer(ephemeral=self.ephemeral).__await__() + return self.do_defer().__await__() async def __aenter__(self) -> None: - await self.ctx.defer(ephemeral=self.ephemeral) + await self.do_defer() async def __aexit__( self, @@ -472,7 +477,7 @@ def permissions(self) -> Permissions: .. versionadded:: 2.0 """ - if self.channel.type is ChannelType.private: + if self.interaction is None and self.channel.type is ChannelType.private: return Permissions._dm_permissions() if not self.interaction: # channel and author will always match relevant types here @@ -506,7 +511,7 @@ def bot_permissions(self) -> Permissions: .. versionadded:: 2.0 """ channel = self.channel - if channel.type == ChannelType.private: + if self.interaction is None and channel.type == ChannelType.private: return Permissions._dm_permissions() if not self.interaction: # channel and me will always match relevant types here @@ -623,6 +628,40 @@ async def send_help(self, *args: Any) -> Any: except CommandError as e: await cmd.on_help_command_error(self, e) + @overload + async def reply( + self, + *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def reply( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def reply( self, @@ -641,6 +680,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -662,6 +702,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -683,6 +724,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -704,6 +746,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -742,7 +785,7 @@ async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: else: return await self.send(content, **kwargs) - def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping]: + def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping[BotT]]: """Returns an asynchronous context manager that allows you to send a typing indicator to the destination for an indefinite period of time, or 10 seconds if the context manager is called using ``await``. @@ -808,6 +851,40 @@ async def defer(self, *, ephemeral: bool = False) -> None: if self.interaction: await self.interaction.response.defer(ephemeral=ephemeral) + @overload + async def send( + self, + *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def send( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def send( self, @@ -826,6 +903,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -847,6 +925,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -868,6 +947,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -889,6 +969,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -907,10 +988,11 @@ async def send( allowed_mentions: Optional[AllowedMentions] = None, reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, - view: Optional[View] = None, + view: Optional[BaseView] = None, suppress_embeds: bool = False, ephemeral: bool = False, silent: bool = False, + poll: Poll = MISSING, ) -> Message: """|coro| @@ -972,10 +1054,12 @@ async def send( This is ignored for interaction based contexts. .. versionadded:: 1.6 - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. embeds: List[:class:`~discord.Embed`] A list of embeds to upload. Must be a maximum of 10. @@ -1000,6 +1084,11 @@ async def send( .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 + Raises -------- ~discord.HTTPException @@ -1037,6 +1126,7 @@ async def send( view=view, suppress_embeds=suppress_embeds, silent=silent, + poll=poll, ) # type: ignore # The overloads don't support Optional but the implementation does # Convert the kwargs from None to MISSING to appease the remaining implementations @@ -1052,13 +1142,17 @@ async def send( 'suppress_embeds': suppress_embeds, 'ephemeral': ephemeral, 'silent': silent, + 'poll': poll, } if self.interaction.response.is_done(): msg = await self.interaction.followup.send(**kwargs, wait=True) else: - await self.interaction.response.send_message(**kwargs) - msg = await self.interaction.original_response() + response = await self.interaction.response.send_message(**kwargs) + if not isinstance(response.resource, discord.InteractionMessage): + msg = await self.interaction.original_response() + else: + msg = response.resource if delete_after is not None: await msg.delete(delay=delete_after) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 7255f171540b..d316f6ccc63c 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -82,6 +82,7 @@ 'GuildChannelConverter', 'GuildStickerConverter', 'ScheduledEventConverter', + 'SoundboardSoundConverter', 'clean_content', 'Greedy', 'Range', @@ -126,6 +127,10 @@ async def convert(self, ctx: Context[BotT], argument: str) -> T_co: raise a :exc:`.CommandError` derived exception as it will properly propagate to the error handlers. + Note that if this method is called manually, :exc:`Exception` + should be caught to handle the cases where a subclass does + not explicitly inherit from :exc:`.CommandError`. + Parameters ----------- ctx: :class:`.Context` @@ -438,19 +443,36 @@ class GuildChannelConverter(IDConverter[discord.abc.GuildChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name. + 3. Lookup by channel URL. + 4. Lookup by name. .. versionadded:: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.abc.GuildChannel: return self._resolve_channel(ctx, argument, 'channels', discord.abc.GuildChannel) + @staticmethod + def _parse_from_url(argument: str) -> Optional[re.Match[str]]: + link_regex = re.compile( + r'https?://(?:(?:ptb|canary|www)\.)?discord(?:app)?\.com/channels/' + r'(?:[0-9]{15,20}|@me)' + r'/([0-9]{15,20})(?:/(?:[0-9]{15,20})/?)?$' + ) + return link_regex.match(argument) + @staticmethod def _resolve_channel(ctx: Context[BotT], argument: str, attribute: str, type: Type[CT]) -> CT: bot = ctx.bot - match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument) + match = ( + IDConverter._get_id_match(argument) + or re.match(r'<#([0-9]{15,20})>$', argument) + or GuildChannelConverter._parse_from_url(argument) + ) result = None guild = ctx.guild @@ -480,7 +502,11 @@ def check(c): @staticmethod def _resolve_thread(ctx: Context[BotT], argument: str, attribute: str, type: Type[TT]) -> TT: - match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument) + match = ( + IDConverter._get_id_match(argument) + or re.match(r'<#([0-9]{15,20})>$', argument) + or GuildChannelConverter._parse_from_url(argument) + ) result = None guild = ctx.guild @@ -510,10 +536,14 @@ class TextChannelConverter(IDConverter[discord.TextChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.TextChannel: @@ -530,10 +560,14 @@ class VoiceChannelConverter(IDConverter[discord.VoiceChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.VoiceChannel: @@ -552,7 +586,11 @@ class StageChannelConverter(IDConverter[discord.StageChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.StageChannel: @@ -569,7 +607,11 @@ class CategoryChannelConverter(IDConverter[discord.CategoryChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` @@ -588,9 +630,13 @@ class ThreadConverter(IDConverter[discord.Thread]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name. + 3. Lookup by channel URL. + 4. Lookup by name. .. versionadded: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.Thread: @@ -607,9 +653,13 @@ class ForumChannelConverter(IDConverter[discord.ForumChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionadded:: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.ForumChannel: @@ -902,6 +952,44 @@ async def convert(self, ctx: Context[BotT], argument: str) -> discord.ScheduledE return result +class SoundboardSoundConverter(IDConverter[discord.SoundboardSound]): + """Converts to a :class:`~discord.SoundboardSound`. + + Lookups are done for the local guild if available. Otherwise, for a DM context, + lookup is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by name. + + .. versionadded:: 2.5 + """ + + async def convert(self, ctx: Context[BotT], argument: str) -> discord.SoundboardSound: + guild = ctx.guild + match = self._get_id_match(argument) + result = None + + if match: + # ID match + sound_id = int(match.group(1)) + if guild: + result = guild.get_soundboard_sound(sound_id) + else: + result = ctx.bot.get_soundboard_sound(sound_id) + else: + # lookup by name + if guild: + result = discord.utils.get(guild.soundboard_sounds, name=argument) + else: + result = discord.utils.get(ctx.bot.soundboard_sounds, name=argument) + if result is None: + raise SoundboardSoundNotFound(argument) + + return result + + class clean_content(Converter[str]): """Converts the argument to mention scrubbed version of said content. @@ -1037,7 +1125,7 @@ def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]: args = getattr(converter, '__args__', ()) if discord.utils.PY_310 and converter.__class__ is types.UnionType: # type: ignore - converter = Union[args] # type: ignore + converter = Union[args] origin = getattr(converter, '__origin__', None) @@ -1050,7 +1138,7 @@ def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]: if origin is Union and type(None) in args: raise TypeError(f'Greedy[{converter!r}] is invalid.') - return cls(converter=converter) + return cls(converter=converter) # type: ignore @property def constructed_converter(self) -> Any: @@ -1185,7 +1273,7 @@ def _convert_to_bool(argument: str) -> bool: raise BadBoolArgument(lowered) -_GenericAlias = type(List[T]) +_GenericAlias = type(List[T]) # type: ignore def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: @@ -1214,6 +1302,7 @@ def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: discord.GuildSticker: GuildStickerConverter, discord.ScheduledEvent: ScheduledEventConverter, discord.ForumChannel: ForumChannelConverter, + discord.SoundboardSound: SoundboardSoundConverter, } @@ -1236,7 +1325,7 @@ async def _actual_conversion(ctx: Context[BotT], converter: Any, argument: str, else: return await converter().convert(ctx, argument) elif isinstance(converter, Converter): - return await converter.convert(ctx, argument) # type: ignore + return await converter.convert(ctx, argument) except CommandError: raise except Exception as exc: diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 9a73a2b4edab..cf328d9b3f5e 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -71,7 +71,7 @@ def get_key(self, msg: Union[Message, Context[Any]]) -> Any: elif self is BucketType.member: return ((msg.guild and msg.guild.id), msg.author.id) elif self is BucketType.category: - return (msg.channel.category or msg.channel).id # type: ignore + return (getattr(msg.channel, 'category', None) or msg.channel).id elif self is BucketType.role: # we return the channel id of a private-channel as there are only roles in guilds # and that yields the same result as for a guild with only the @everyone role diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index efd7b09d29fc..dc2b96ebf5ba 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -24,6 +24,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass import datetime import functools import inspect @@ -42,6 +43,8 @@ Type, TypeVar, Union, + get_args, + get_origin, overload, ) import re @@ -292,6 +295,11 @@ def __next__(self) -> discord.Attachment: def is_empty(self) -> bool: return self.index >= len(self.data) +@dataclass +class CommandArgument: + name: str + optional: bool + class Command(_BaseCommand, Generic[CogT, P, T]): r"""A class that implements the protocol for a bot text command. @@ -461,7 +469,7 @@ def __init__( # bandaid for the fact that sometimes parent can be the bot instance parent: Optional[GroupMixin[Any]] = kwargs.get('parent') - self.parent: Optional[GroupMixin[Any]] = parent if isinstance(parent, _BaseCommand) else None # type: ignore # Does not recognise mixin usage + self.parent: Optional[GroupMixin[Any]] = parent if isinstance(parent, _BaseCommand) else None self._before_invoke: Optional[Hook] = None try: @@ -512,6 +520,37 @@ def callback( self.params: Dict[str, Parameter] = get_signature_parameters(function, globalns) + @discord.utils.cached_property + def arguments(self: Command) -> List[CommandArgument]: + def get_parameter_name(name: str, annotation: Optional[type]) -> str: + if annotation is not None and get_origin(annotation) is Literal: + literal_values = get_args(annotation) + if len(literal_values) > 1: + return ', '.join([f"'{value}'" for value in literal_values[:-1]]) + f" or '{literal_values[-1]}'" + else: + return f'`{literal_values[0]}`' # type: ignore + return name.replace('_', ' ') + + return [ + CommandArgument( + name=get_parameter_name(name, param.annotation), + optional=(get_origin(param.annotation) is Union and type(None) in get_args(param.annotation)), + ) + for name, param in list(inspect.signature(self.callback).parameters.items())[2:] + ] + + @discord.utils.cached_property + def permissions(self) -> List[str]: + return [ + perm + for check in self.checks + if getattr(check, '__closure__', None) + for cell in check.__closure__ # type: ignore + if isinstance(cell.cell_contents, dict) + for perm, val in cell.cell_contents.items() + if val + ] or ['N/A'] + def add_check(self, func: UserCheck[Context[Any]], /) -> None: """Adds a check to the command. @@ -776,7 +815,7 @@ def full_parent_name(self) -> str: command = self # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO while command.parent is not None: # type: ignore - command = command.parent + command = command.parent # type: ignore entries.append(command.name) # type: ignore return ' '.join(reversed(entries)) @@ -794,7 +833,7 @@ def parents(self) -> List[Group[Any, ..., Any]]: entries = [] command = self while command.parent is not None: # type: ignore - command = command.parent + command = command.parent # type: ignore entries.append(command) return entries @@ -1285,7 +1324,7 @@ async def can_run(self, ctx: Context[BotT], /) -> bool: # since we have no checks, then we just return True. return True - return await discord.utils.async_all(predicate(ctx) for predicate in predicates) + return await discord.utils.async_all(predicate(ctx) for predicate in predicates) # type: ignore finally: ctx.command = original diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 0c1e0f2d0db4..feb4aee279cd 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -24,18 +24,19 @@ 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, Generic from discord.errors import ClientException, DiscordException from discord.utils import _human_join +from ._types import BotT + if TYPE_CHECKING: from discord.abc import GuildChannel from discord.threads import Thread from discord.types.snowflake import Snowflake, SnowflakeList from discord.app_commands import AppCommandError - from ._types import BotT from .context import Context from .converter import Converter from .cooldowns import BucketType, Cooldown @@ -75,6 +76,7 @@ 'EmojiNotFound', 'GuildStickerNotFound', 'ScheduledEventNotFound', + 'SoundboardSoundNotFound', 'PartialEmojiConversionFailure', 'BadBoolArgument', 'MissingRole', @@ -234,7 +236,7 @@ class CheckFailure(CommandError): pass -class CheckAnyFailure(CheckFailure): +class CheckAnyFailure(Generic[BotT], CheckFailure): """Exception raised when all predicates in :func:`check_any` fail. This inherits from :exc:`CheckFailure`. @@ -564,6 +566,24 @@ def __init__(self, argument: str) -> None: super().__init__(f'ScheduledEvent "{argument}" not found.') +class SoundboardSoundNotFound(BadArgument): + """Exception raised when the bot can not find the soundboard sound. + + This inherits from :exc:`BadArgument` + + .. versionadded:: 2.5 + + Attributes + ----------- + argument: :class:`str` + The sound supplied by the caller that was not found + """ + + def __init__(self, argument: str) -> None: + self.argument: str = argument + super().__init__(f'SoundboardSound "{argument}" not found.') + + class BadBoolArgument(BadArgument): """Exception raised when a boolean argument was not convertable. @@ -1061,7 +1081,7 @@ class ExtensionNotFound(ExtensionError): """ def __init__(self, name: str) -> None: - msg = f'Extension {name!r} could not be loaded.' + msg = f'Extension {name!r} could not be loaded or found.' super().__init__(msg, name=name) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 280b652573ef..0766ecae34ea 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -79,6 +79,10 @@ class Flag: description: :class:`str` The description of the flag. Shown for hybrid commands when they're used as application commands. + positional: :class:`bool` + Whether the flag is positional or not. There can only be one positional flag. + + .. versionadded:: 2.4 """ name: str = MISSING @@ -89,6 +93,7 @@ class Flag: max_args: int = MISSING override: bool = MISSING description: str = MISSING + positional: bool = MISSING cast_to_dict: bool = False @property @@ -109,6 +114,7 @@ def flag( override: bool = MISSING, converter: Any = MISSING, description: str = MISSING, + positional: bool = MISSING, ) -> Any: """Override default functionality and parameters of the underlying :class:`FlagConverter` class attributes. @@ -136,6 +142,10 @@ class attributes. description: :class:`str` The description of the flag. Shown for hybrid commands when they're used as application commands. + positional: :class:`bool` + Whether the flag is positional or not. There can only be one positional flag. + + .. versionadded:: 2.4 """ return Flag( name=name, @@ -145,6 +155,7 @@ class attributes. override=override, annotation=converter, description=description, + positional=positional, ) @@ -171,6 +182,7 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s flags: Dict[str, Flag] = {} cache: Dict[str, Any] = {} names: Set[str] = set() + positional: Optional[Flag] = None for name, annotation in annotations.items(): flag = namespace.pop(name, MISSING) if isinstance(flag, Flag): @@ -183,6 +195,11 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s if flag.name is MISSING: flag.name = name + if flag.positional: + if positional is not None: + raise TypeError(f"{flag.name!r} positional flag conflicts with {positional.name!r} flag.") + positional = flag + annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache) if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible(): @@ -270,6 +287,7 @@ class FlagsMeta(type): __commands_flag_case_insensitive__: bool __commands_flag_delimiter__: str __commands_flag_prefix__: str + __commands_flag_positional__: Optional[Flag] def __new__( cls, @@ -280,7 +298,7 @@ def __new__( case_insensitive: bool = MISSING, delimiter: str = MISSING, prefix: str = MISSING, - ) -> Self: + ) -> FlagsMeta: attrs['__commands_is_flag__'] = True try: @@ -324,9 +342,13 @@ def __new__( delimiter = attrs.setdefault('__commands_flag_delimiter__', ':') prefix = attrs.setdefault('__commands_flag_prefix__', '') + positional: Optional[Flag] = None for flag_name, flag in get_flags(attrs, global_ns, local_ns).items(): flags[flag_name] = flag aliases.update({alias_name: flag_name for alias_name in flag.aliases}) + if flag.positional: + positional = flag + attrs['__commands_flag_positional__'] = positional forbidden = set(delimiter).union(prefix) for flag_name in flags: @@ -421,7 +443,7 @@ async def convert_flag(ctx: Context[BotT], argument: str, flag: Flag, annotation return await convert_flag(ctx, argument, flag, annotation) elif origin is Union and type(None) in annotation.__args__: # typing.Optional[x] - annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))] # type: ignore + annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))] return await run_converters(ctx, annotation, argument, param) elif origin is dict: # typing.Dict[K, V] -> typing.Tuple[K, V] @@ -500,10 +522,25 @@ def parse_flags(cls, argument: str, *, ignore_extra: bool = True) -> Dict[str, L result: Dict[str, List[str]] = {} flags = cls.__commands_flags__ aliases = cls.__commands_flag_aliases__ + positional_flag = cls.__commands_flag_positional__ last_position = 0 last_flag: Optional[Flag] = None case_insensitive = cls.__commands_flag_case_insensitive__ + + if positional_flag is not None: + match = cls.__commands_flag_regex__.search(argument) + if match is not None: + begin, end = match.span(0) + value = argument[:begin].strip() + else: + value = argument.strip() + last_position = len(argument) + + if value: + name = positional_flag.name.casefold() if case_insensitive else positional_flag.name + result[name] = [value] + for match in cls.__commands_flag_regex__.finditer(argument): begin, end = match.span(0) key = match.group('flag') diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 163bc2694936..d06fbd8bf27d 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -297,6 +297,11 @@ def _eject_cog(self) -> None: # Revert `on_error` to use the original one in case of race conditions self.on_error = self._injected.on_help_command_error + def update(self, **kwargs: Any) -> None: + cog = self.cog + self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs)) + self.cog = cog + class HelpCommand: r"""The base implementation for help command formatting. @@ -377,9 +382,8 @@ def copy(self) -> Self: return obj def _add_to_bot(self, bot: BotBase) -> None: - command = _HelpCommandImpl(self, **self.command_attrs) - bot.add_command(command) - self._command_impl = command + self._command_impl.update(**self.command_attrs) + bot.add_command(self._command_impl) def _remove_from_bot(self, bot: BotBase) -> None: bot.remove_command(self._command_impl.name) diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index a8775474dc9a..0857003fad90 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -43,7 +43,7 @@ from discord import app_commands from discord.utils import MISSING, maybe_coroutine, async_all from .core import Command, Group -from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError +from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError, DisabledCommand from .converter import Converter, Range, Greedy, run_converters, CONVERTER_MAPPING from .parameters import Parameter from .flags import is_flag, FlagConverter @@ -203,9 +203,9 @@ def replace_parameter( # Fallback to see if the behaviour needs changing origin = getattr(converter, '__origin__', None) args = getattr(converter, '__args__', []) - if isinstance(converter, Range): + if isinstance(converter, Range): # type: ignore # Range is not an Annotation at runtime r = converter - param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) + param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) # type: ignore elif isinstance(converter, Greedy): # Greedy is "optional" in ext.commands # However, in here, it probably makes sense to make it required. @@ -234,6 +234,12 @@ def replace_parameter( descriptions[name] = flag.description if flag.name != flag.attribute: renames[name] = flag.name + if pseudo.default is not pseudo.empty: + # This ensures the default is wrapped around _CallableDefault if callable + # else leaves it as-is. + pseudo = pseudo.replace( + default=_CallableDefault(flag.default) if callable(flag.default) else flag.default + ) mapping[name] = pseudo @@ -251,7 +257,7 @@ def replace_parameter( inner = args[0] is_inner_transformer = is_transformer(inner) if is_converter(inner) and not is_inner_transformer: - param = param.replace(annotation=Optional[ConverterTransformer(inner, original)]) # type: ignore + param = param.replace(annotation=Optional[ConverterTransformer(inner, original)]) else: raise elif origin: @@ -283,7 +289,7 @@ def replace_parameters( param = param.replace(default=default) if isinstance(param.default, Parameter): - # If we're here, then then it hasn't been handled yet so it should be removed completely + # If we're here, then it hasn't been handled yet so it should be removed completely param = param.replace(default=parameter.empty) # Flags are flattened out and thus don't get their parameter in the actual mapping @@ -418,10 +424,10 @@ async def _check_can_run(self, interaction: discord.Interaction) -> bool: if not ret: return False - if self.checks and not await async_all(f(interaction) for f in self.checks): + if self.checks and not await async_all(f(interaction) for f in self.checks): # type: ignore return False - if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): + if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): # type: ignore return False return True @@ -526,6 +532,9 @@ def cog(self, value: CogT) -> None: self.app_command.binding = value async def can_run(self, ctx: Context[BotT], /) -> bool: + if not self.enabled: + raise DisabledCommand(f'{self.name} command is disabled') + if ctx.interaction is not None and self.app_command: return await self.app_command._check_can_run(ctx.interaction) else: @@ -653,6 +662,8 @@ def __init__( guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False) default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None) nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__', False) + contexts = getattr(self.callback, '__discord_app_commands_contexts__', MISSING) + installs = getattr(self.callback, '__discord_app_commands_installation_types__', MISSING) self.app_command = app_commands.Group( name=self._locale_name or self.name, description=self._locale_description or self.description or self.short_doc or '…', @@ -660,6 +671,8 @@ def __init__( guild_only=guild_only, default_permissions=default_permissions, nsfw=nsfw, + allowed_installs=installs, + allowed_contexts=contexts, ) # This prevents the group from re-adding the command at __init__ @@ -902,7 +915,8 @@ def hybrid_command( def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridCommand[CogT, P, T]: if isinstance(func, Command): raise TypeError('Callback is already a command.') - return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # ??? + # Pyright does not allow Command[Any] to be assigned to Command[CogT] despite it being okay here + return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore return decorator diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py index 70df39534aef..196530d94c8b 100644 --- a/discord/ext/commands/parameters.py +++ b/discord/ext/commands/parameters.py @@ -135,7 +135,7 @@ def replace( if displayed_name is MISSING: displayed_name = self._displayed_name - return self.__class__( + ret = self.__class__( name=name, kind=kind, default=default, @@ -144,6 +144,8 @@ def replace( displayed_default=displayed_default, displayed_name=displayed_name, ) + ret._fallback = self._fallback + return ret if not TYPE_CHECKING: # this is to prevent anything breaking if inspect internals change name = _gen_property('name') @@ -247,6 +249,12 @@ async def wave(ctx, to: discord.User = commands.parameter(default=lambda ctx: ct .. versionadded:: 2.3 """ + if isinstance(default, Parameter): + if displayed_default is empty: + displayed_default = default._displayed_default + + default = default._default + return Parameter( name='empty', kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 81644da3aa44..57f9e741bc6b 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -111,12 +111,17 @@ def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> self.loop: asyncio.AbstractEventLoop = loop self.future: asyncio.Future[None] = loop.create_future() relative_delta = discord.utils.compute_timedelta(dt) - self.handle = loop.call_later(relative_delta, self.future.set_result, None) + self.handle = loop.call_later(relative_delta, self._wrapped_set_result, self.future) + + @staticmethod + def _wrapped_set_result(future: asyncio.Future) -> None: + if not future.done(): + future.set_result(None) def recalculate(self, dt: datetime.datetime) -> None: self.handle.cancel() relative_delta = discord.utils.compute_timedelta(dt) - self.handle: asyncio.TimerHandle = self.loop.call_later(relative_delta, self.future.set_result, None) + self.handle: asyncio.TimerHandle = self.loop.call_later(relative_delta, self._wrapped_set_result, self.future) def wait(self) -> asyncio.Future[Any]: return self.future diff --git a/discord/file.py b/discord/file.py index 504d86b73a80..7e4df415b241 100644 --- a/discord/file.py +++ b/discord/file.py @@ -111,7 +111,7 @@ def __init__( else: filename = getattr(fp, 'name', 'untitled') - self._filename, filename_spoiler = _strip_spoiler(filename) + self._filename, filename_spoiler = _strip_spoiler(filename) # type: ignore # pyright doesn't understand the above getattr if spoiler is MISSING: spoiler = filename_spoiler diff --git a/discord/flags.py b/discord/flags.py index 6e5721fcf39a..8bf4ee9c7326 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -58,9 +58,12 @@ 'ChannelFlags', 'AutoModPresets', 'MemberFlags', + 'AppCommandContext', 'AttachmentFlags', 'RoleFlags', + 'AppInstallationType', 'SKUFlags', + 'EmbedFlags', ) BF = TypeVar('BF', bound='BaseFlags') @@ -133,7 +136,7 @@ def __init__(self, **kwargs: bool): setattr(self, key, value) @classmethod - def _from_value(cls, value): + def _from_value(cls, value: int) -> Self: self = cls.__new__(cls) self.value = value return self @@ -488,6 +491,24 @@ def voice(self): """ return 8192 + @flag_value + def forwarded(self): + """:class:`bool`: Returns ``True`` if the message is a forwarded message. + + .. versionadded:: 2.5 + """ + return 16384 + + @flag_value + def components_v2(self): + """:class:`bool`: Returns ``True`` if the message has Discord's v2 components. + + Does not allow sending any ``content``, ``embed``, ``embeds``, ``stickers``, or ``poll``. + + .. versionadded:: 2.6 + """ + return 32768 + @fill_with_flags() class PublicUserFlags(BaseFlags): @@ -869,34 +890,52 @@ def bans(self): @alias_flag_value def emojis(self): - """:class:`bool`: Alias of :attr:`.emojis_and_stickers`. + """:class:`bool`: Alias of :attr:`.expressions`. .. versionchanged:: 2.0 Changed to an alias. """ return 1 << 3 - @flag_value + @alias_flag_value def emojis_and_stickers(self): - """:class:`bool`: Whether guild emoji and sticker related events are enabled. + """:class:`bool`: Alias of :attr:`.expressions`. .. versionadded:: 2.0 + .. versionchanged:: 2.5 + Changed to an alias. + """ + return 1 << 3 + + @flag_value + def expressions(self): + """:class:`bool`: Whether guild emoji, sticker, and soundboard sound related events are enabled. + + .. versionadded:: 2.5 + This corresponds to the following events: - :func:`on_guild_emojis_update` - :func:`on_guild_stickers_update` + - :func:`on_soundboard_sound_create` + - :func:`on_soundboard_sound_update` + - :func:`on_soundboard_sound_delete` This also corresponds to the following attributes and classes in terms of cache: - :class:`Emoji` - :class:`GuildSticker` + - :class:`SoundboardSound` - :meth:`Client.get_emoji` - :meth:`Client.get_sticker` + - :meth:`Client.get_soundboard_sound` - :meth:`Client.emojis` - :meth:`Client.stickers` + - :meth:`Client.soundboard_sounds` - :attr:`Guild.emojis` - :attr:`Guild.stickers` + - :attr:`Guild.soundboard_sounds` """ return 1 << 3 @@ -1255,6 +1294,57 @@ def auto_moderation_execution(self): """ return 1 << 21 + @alias_flag_value + def polls(self): + """:class:`bool`: Whether guild and direct messages poll related events are enabled. + + This is a shortcut to set or get both :attr:`guild_polls` and :attr:`dm_polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (both guilds and DMs) + - :func:`on_poll_vote_remove` (both guilds and DMs) + - :func:`on_raw_poll_vote_add` (both guilds and DMs) + - :func:`on_raw_poll_vote_remove` (both guilds and DMs) + + .. versionadded:: 2.4 + """ + return (1 << 24) | (1 << 25) + + @flag_value + def guild_polls(self): + """:class:`bool`: Whether guild poll related events are enabled. + + See also :attr:`dm_polls` and :attr:`polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for guilds) + - :func:`on_poll_vote_remove` (only for guilds) + - :func:`on_raw_poll_vote_add` (only for guilds) + - :func:`on_raw_poll_vote_remove` (only for guilds) + + .. versionadded:: 2.4 + """ + return 1 << 24 + + @flag_value + def dm_polls(self): + """:class:`bool`: Whether direct messages poll related events are enabled. + + See also :attr:`guild_polls` and :attr:`polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for DMs) + - :func:`on_poll_vote_remove` (only for DMs) + - :func:`on_raw_poll_vote_add` (only for DMs) + - :func:`on_raw_poll_vote_remove` (only for DMs) + + .. versionadded:: 2.4 + """ + return 1 << 25 + @fill_with_flags() class MemberCacheFlags(BaseFlags): @@ -1660,8 +1750,24 @@ def _from_value(cls: Type[Self], value: Sequence[int]) -> Self: self.value = reduce(or_, map((1).__lshift__, value), 0) >> 1 return self - def to_array(self) -> List[int]: - return [i + 1 for i in range(self.value.bit_length()) if self.value & (1 << i)] + def to_array(self, *, offset: int = 0) -> List[int]: + return [i + offset for i in range(self.value.bit_length()) if self.value & (1 << i)] + + @classmethod + def all(cls: Type[Self]) -> Self: + """A factory method that creates an instance of ArrayFlags with everything enabled.""" + bits = max(cls.VALID_FLAGS.values()).bit_length() + value = (1 << bits) - 1 + self = cls.__new__(cls) + self.value = value + return self + + @classmethod + def none(cls: Type[Self]) -> Self: + """A factory method that creates an instance of ArrayFlags with everything disabled.""" + self = cls.__new__(cls) + self.value = self.DEFAULT_VALUE + return self @fill_with_flags() @@ -1728,6 +1834,9 @@ class AutoModPresets(ArrayFlags): rather than using this raw value. """ + def to_array(self) -> List[int]: + return super().to_array(offset=1) + @flag_value def profanity(self): """:class:`bool`: Whether to use the preset profanity filter.""" @@ -1743,21 +1852,144 @@ def slurs(self): """:class:`bool`: Whether to use the preset slurs filter.""" return 1 << 2 - @classmethod - def all(cls: Type[Self]) -> Self: - """A factory method that creates a :class:`AutoModPresets` with everything enabled.""" - bits = max(cls.VALID_FLAGS.values()).bit_length() - value = (1 << bits) - 1 - self = cls.__new__(cls) - self.value = value - return self - @classmethod - def none(cls: Type[Self]) -> Self: - """A factory method that creates a :class:`AutoModPresets` with everything disabled.""" - self = cls.__new__(cls) - self.value = self.DEFAULT_VALUE - return self +@fill_with_flags() +class AppCommandContext(ArrayFlags): + r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two AppCommandContext flags are equal. + + .. describe:: x != y + + Checks if two AppCommandContext flags are not equal. + + .. describe:: x | y, x |= y + + Returns an AppCommandContext instance with all enabled flags from + both x and y. + + .. describe:: x & y, x &= y + + Returns an AppCommandContext instance with only flags enabled on + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an AppCommandContext instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an AppCommandContext 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. + """ + + DEFAULT_VALUE = 3 + + @flag_value + def guild(self): + """:class:`bool`: Whether the context allows usage in a guild.""" + return 1 << 0 + + @flag_value + def dm_channel(self): + """:class:`bool`: Whether the context allows usage in a DM channel.""" + return 1 << 1 + + @flag_value + def private_channel(self): + """:class:`bool`: Whether the context allows usage in a DM or a GDM channel.""" + return 1 << 2 + + +@fill_with_flags() +class AppInstallationType(ArrayFlags): + r"""Represents the installation location of an application command. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two AppInstallationType flags are equal. + + .. describe:: x != y + + Checks if two AppInstallationType flags are not equal. + + .. describe:: x | y, x |= y + + Returns an AppInstallationType instance with all enabled flags from + both x and y. + + .. describe:: x & y, x &= y + + Returns an AppInstallationType instance with only flags enabled on + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an AppInstallationType instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an AppInstallationType 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 guild(self): + """:class:`bool`: Whether the integration is a guild install.""" + return 1 << 0 + + @flag_value + def user(self): + """:class:`bool`: Whether the integration is a user install.""" + return 1 << 1 @fill_with_flags() @@ -1837,6 +2069,48 @@ def started_onboarding(self): """:class:`bool`: Returns ``True`` if the member has started onboarding.""" return 1 << 3 + @flag_value + def guest(self): + """:class:`bool`: Returns ``True`` if the member is a guest and can only access + the voice channel they were invited to. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def started_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has started Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 5 + + @flag_value + def completed_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has completed Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 6 + + @flag_value + def automod_quarantined_username(self): + """:class:`bool`: Returns ``True`` if the member's username, nickname, or global name has been + blocked by AutoMod. + + .. versionadded:: 2.5 + """ + return 1 << 7 + + @flag_value + def dm_settings_upsell_acknowledged(self): + """:class:`bool`: Returns ``True`` if the member has dismissed the DM settings upsell. + + .. versionadded:: 2.5 + """ + return 1 << 9 + @fill_with_flags() class AttachmentFlags(BaseFlags): @@ -1910,6 +2184,30 @@ def remix(self): """:class:`bool`: Returns ``True`` if the attachment has been edited using the remix feature.""" return 1 << 2 + @flag_value + def spoiler(self): + """:class:`bool`: Returns ``True`` if the attachment was marked as a spoiler. + + .. versionadded:: 2.5 + """ + return 1 << 3 + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def animated(self): + """:class:`bool`: Returns ``True`` if the attachment is an animated image. + + .. versionadded:: 2.5 + """ + return 1 << 5 + @fill_with_flags() class RoleFlags(BaseFlags): @@ -2045,3 +2343,67 @@ def guild_subscription(self): def user_subscription(self): """:class:`bool`: Returns ``True`` if the SKU is a user subscription.""" return 1 << 8 + + +@fill_with_flags() +class EmbedFlags(BaseFlags): + r"""Wraps up the Discord Embed flags + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two EmbedFlags are equal. + + .. describe:: x != y + + Checks if two EmbedFlags are not equal. + + .. describe:: x | y, x |= y + + Returns an EmbedFlags instance with all enabled flags from + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an EmbedFlags instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an EmbedFlags instance with all flags inverted from x. + + .. describe:: hash(x) + + Returns 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 contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the embed was flagged as sensitive content.""" + return 1 << 4 + + @flag_value + def content_inventory_entry(self): + """:class:`bool`: Returns ``True`` if the embed is a reply to an activity card, and is no + longer displayed. + """ + return 1 << 5 diff --git a/discord/gateway.py b/discord/gateway.py index dc52828134a9..8e753af58512 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -32,7 +33,6 @@ import time import threading import traceback -import zlib from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar, Tuple @@ -212,6 +212,9 @@ def ack(self) -> None: class VoiceKeepAliveHandler(KeepAliveHandler): + if TYPE_CHECKING: + ws: DiscordVoiceWebSocket + def __init__(self, *args: Any, **kwargs: Any) -> None: name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}') super().__init__(*args, name=name, **kwargs) @@ -223,7 +226,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def get_payload(self) -> Dict[str, Any]: return { 'op': self.ws.HEARTBEAT, - 'd': int(time.time() * 1000), + 'd': { + 't': int(time.time() * 1000), + 'seq_ack': self.ws.seq_ack, + }, } def ack(self) -> None: @@ -295,19 +301,19 @@ class DiscordWebSocket: # fmt: off DEFAULT_GATEWAY = yarl.URL('wss://gateway.discord.gg/') - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE = 3 - VOICE_STATE = 4 - VOICE_PING = 5 - RESUME = 6 - RECONNECT = 7 - REQUEST_MEMBERS = 8 - INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE = 3 + VOICE_STATE = 4 + VOICE_PING = 5 + RESUME = 6 + RECONNECT = 7 + REQUEST_MEMBERS = 8 + INVALIDATE_SESSION = 9 + HELLO = 10 + HEARTBEAT_ACK = 11 + GUILD_SYNC = 12 # fmt: on def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None: @@ -325,8 +331,7 @@ def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.Abs # ws related stuff self.session_id: Optional[str] = None self.sequence: Optional[int] = None - self._zlib: zlib._Decompress = zlib.decompressobj() - self._buffer: bytearray = bytearray() + self._decompressor: utils._DecompressionContext = utils._ActiveDecompressionContext() self._close_code: Optional[int] = None self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter() @@ -355,7 +360,7 @@ async def from_client( sequence: Optional[int] = None, resume: bool = False, encoding: str = 'json', - zlib: bool = True, + compress: bool = True, ) -> Self: """Creates a main websocket for Discord from a :class:`Client`. @@ -366,10 +371,12 @@ async def from_client( gateway = gateway or cls.DEFAULT_GATEWAY - if zlib: - url = gateway.with_query(v=INTERNAL_API_VERSION, encoding=encoding, compress='zlib-stream') - else: + if not compress: url = gateway.with_query(v=INTERNAL_API_VERSION, encoding=encoding) + else: + url = gateway.with_query( + v=INTERNAL_API_VERSION, encoding=encoding, compress=utils._ActiveDecompressionContext.COMPRESSION_TYPE + ) socket = await client.http.ws_connect(str(url)) ws = cls(socket, loop=client.loop) @@ -488,13 +495,11 @@ async def resume(self) -> None: async def received_message(self, msg: Any, /) -> None: if type(msg) is bytes: - self._buffer.extend(msg) + msg = self._decompressor.decompress(msg) - if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff': + # Received a partial gateway message + if msg is None: return - msg = self._zlib.decompress(self._buffer) - msg = msg.decode('utf-8') - self._buffer = bytearray() self.log_receive(msg) msg = utils._from_json(msg) @@ -607,7 +612,10 @@ def latency(self) -> float: def _can_handle_close(self) -> bool: code = self._close_code or self.socket.close_code - return code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014) + # If the socket is closed remotely with 1000 and it's not our own explicit close + # then it's an improper close that should be handled and reconnected + is_improper_close = self._close_code is None and self.socket.close_code == 1000 + return is_improper_close or code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014) async def poll_event(self) -> None: """Polls for a DISPATCH event and handles the general gateway loop. @@ -827,9 +835,11 @@ def __init__( self.loop: asyncio.AbstractEventLoop = loop self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._close_code: Optional[int] = None - self.secret_key: Optional[str] = None + self.secret_key: Optional[List[int]] = None + # defaulting to -1 + self.seq_ack: int = -1 if hook: - self._hook = hook + self._hook = hook # type: ignore async def _hook(self, *args: Any) -> None: pass @@ -848,6 +858,7 @@ async def resume(self) -> None: 'token': state.token, 'server_id': str(state.server_id), 'session_id': state.session_id, + 'seq_ack': self.seq_ack, }, } await self.send_as_json(payload) @@ -872,14 +883,16 @@ async def from_connection_state( *, resume: bool = False, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None, + seq_ack: int = -1, ) -> Self: """Creates a voice websocket for the :class:`VoiceClient`.""" - gateway = f'wss://{state.endpoint}/?v=4' + gateway = f'wss://{state.endpoint}/?v=8' client = state.voice_client http = client._state.http socket = await http.ws_connect(gateway, compress=15) ws = cls(socket, loop=client.loop, hook=hook) ws.gateway = gateway + ws.seq_ack = seq_ack ws._connection = state ws._max_heartbeat_timeout = 60.0 ws.thread_id = threading.get_ident() @@ -891,7 +904,7 @@ async def from_connection_state( return ws - async def select_protocol(self, ip: str, port: int, mode: int) -> None: + async def select_protocol(self, ip: str, port: int, mode: str) -> None: payload = { 'op': self.SELECT_PROTOCOL, 'd': { @@ -932,6 +945,7 @@ async def received_message(self, msg: Dict[str, Any]) -> None: _log.debug('Voice websocket frame received: %s', msg) op = msg['op'] data = msg['d'] # According to Discord this key is always given + self.seq_ack = msg.get('seq', self.seq_ack) # this key could not be given if op == self.READY: await self.initial_connection(data) @@ -1041,4 +1055,4 @@ async def close(self, code: int = 1000) -> None: self._keep_alive.stop() self._close_code = code - await self.ws.close(code=code) + await self.ws.close(code=code) \ No newline at end of file diff --git a/discord/guild.py b/discord/guild.py index 82692ff73388..2991b89f88a1 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -34,6 +34,7 @@ Collection, Coroutine, Dict, + Iterable, List, Mapping, NamedTuple, @@ -93,10 +94,12 @@ from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji - +from .soundboard import SoundboardSound +from .presences import RawPresenceUpdateEvent __all__ = ( 'Guild', + 'GuildPreview', 'BanEntry', ) @@ -107,8 +110,10 @@ from .types.guild import ( Ban as BanPayload, Guild as GuildPayload, + GuildPreview as GuildPreviewPayload, RolePositionUpdate as RolePositionUpdatePayload, GuildFeature, + IncidentData, ) from .types.threads import ( Thread as ThreadPayload, @@ -145,6 +150,11 @@ class BanEntry(NamedTuple): user: User +class BulkBanResult(NamedTuple): + banned: List[Object] + failed: List[Object] + + class _GuildLimit(NamedTuple): emoji: int stickers: int @@ -152,6 +162,121 @@ class _GuildLimit(NamedTuple): filesize: int +class GuildPreview(Hashable): + """Represents a preview of a Discord guild. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two guild previews are equal. + + .. describe:: x != y + + Checks if two guild previews are not equal. + + .. describe:: hash(x) + + Returns the guild's hash. + + .. describe:: str(x) + + Returns the guild's name. + + Attributes + ---------- + name: :class:`str` + The guild preview's name. + id: :class:`int` + The guild preview's ID. + features: List[:class:`str`] + A list of features the guild has. See :attr:`Guild.features` for more information. + description: Optional[:class:`str`] + The guild preview's description. + emojis: Tuple[:class:`Emoji`, ...] + All emojis that the guild owns. + stickers: Tuple[:class:`GuildSticker`, ...] + All stickers that the guild owns. + approximate_member_count: :class:`int` + The approximate number of members in the guild. + approximate_presence_count: :class:`int` + The approximate number of members currently active in in the guild. Offline members are excluded. + """ + + __slots__ = ( + '_state', + '_icon', + '_splash', + '_discovery_splash', + 'id', + 'name', + 'emojis', + 'stickers', + 'features', + 'description', + "approximate_member_count", + "approximate_presence_count", + ) + + def __init__(self, *, data: GuildPreviewPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.id = int(data['id']) + self.name: str = data['name'] + self._icon: Optional[str] = data.get('icon') + self._splash: Optional[str] = data.get('splash') + self._discovery_splash: Optional[str] = data.get('discovery_splash') + self.emojis: Tuple[Emoji, ...] = tuple( + map( + lambda d: Emoji(guild=state._get_or_create_unavailable_guild(self.id), state=state, data=d), + data.get('emojis', []), + ) + ) + self.stickers: Tuple[GuildSticker, ...] = tuple( + map(lambda d: GuildSticker(state=state, data=d), data.get('stickers', [])) + ) + self.features: List[GuildFeature] = data.get('features', []) + self.description: Optional[str] = data.get('description') + self.approximate_member_count: int = data.get('approximate_member_count') + self.approximate_presence_count: int = data.get('approximate_presence_count') + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return ( + f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r} ' + f'features={self.features}>' + ) + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the guild's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def icon(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + if self._icon is None: + return None + return Asset._from_guild_icon(self._state, self.id, self._icon) + + @property + def splash(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + if self._splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes') + + @property + def discovery_splash(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available.""" + if self._discovery_splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._discovery_splash, path='discovery-splashes') + + class Guild(Hashable): """Represents a Discord guild. @@ -320,6 +445,8 @@ class Guild(Hashable): 'premium_progress_bar_enabled', '_safety_alerts_channel_id', 'max_stage_video_users', + '_incidents_data', + '_soundboard_sounds', ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { @@ -337,6 +464,7 @@ def __init__(self, *, data: GuildPayload, state: ConnectionState) -> None: self._threads: Dict[int, Thread] = {} self._stage_instances: Dict[int, StageInstance] = {} self._scheduled_events: Dict[int, ScheduledEvent] = {} + self._soundboard_sounds: Dict[int, SoundboardSound] = {} self._state: ConnectionState = state self._member_count: Optional[int] = None self._from_data(data) @@ -370,10 +498,11 @@ def _remove_thread(self, thread: Snowflake, /) -> None: def _clear_threads(self) -> None: self._threads.clear() - def _remove_threads_by_channel(self, channel_id: int) -> None: - to_remove = [k for k, t in self._threads.items() if t.parent_id == channel_id] - for k in to_remove: - del self._threads[k] + def _remove_threads_by_channel(self, channel_id: int) -> List[Thread]: + to_remove = [t for t in self._threads.values() if t.parent_id == channel_id] + for thread in to_remove: + del self._threads[thread.id] + return to_remove def _filter_threads(self, channel_ids: Set[int]) -> Dict[int, Thread]: to_remove: Dict[int, Thread] = {k: t for k, t in self._threads.items() if t.parent_id in channel_ids} @@ -381,6 +510,12 @@ def _filter_threads(self, channel_ids: Set[int]) -> Dict[int, Thread]: del self._threads[k] return to_remove + def _add_soundboard_sound(self, sound: SoundboardSound, /) -> None: + self._soundboard_sounds[sound.id] = sound + + def _remove_soundboard_sound(self, sound: SoundboardSound, /) -> None: + self._soundboard_sounds.pop(sound.id, None) + def __str__(self) -> str: return self.name or '' @@ -416,42 +551,30 @@ def _update_voice_state(self, data: GuildVoiceState, channel_id: int) -> Tuple[O member = self.get_member(user_id) if member is None: try: - member = Member(data=data['member'], state=self._state, guild=self) + member_data = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] + member = Member(data=member_data, state=self._state, guild=self) except KeyError: member = None return member, before, after def _add_role(self, role: Role, /) -> None: - # roles get added to the bottom (position 1, pos 0 is @everyone) - # so since self.roles has the @everyone role, we can't increment - # its position because it's stuck at position 0. Luckily x += False - # is equivalent to adding 0. So we cast the position to a bool and - # increment it. - for r in self._roles.values(): - r.position += not r.is_default() - self._roles[role.id] = role def _remove_role(self, role_id: int, /) -> Role: # this raises KeyError if it fails.. - role = self._roles.pop(role_id) - - # since it didn't, we can change the positions now - # basically the same as above except we only decrement - # the position if we're above the role we deleted. - for r in self._roles.values(): - r.position -= r.position > role.position - - return role + return self._roles.pop(role_id) @classmethod - def _create_unavailable(cls, *, state: ConnectionState, guild_id: int) -> Guild: - return cls(state=state, data={'id': guild_id, 'unavailable': True}) # type: ignore + def _create_unavailable(cls, *, state: ConnectionState, guild_id: int, data: Optional[Dict[str, Any]]) -> Guild: + if data is None: + data = {'unavailable': True} + data.update(id=guild_id) + return cls(state=state, data=data) # type: ignore def _from_data(self, guild: GuildPayload) -> None: try: - self._member_count = guild['member_count'] + self._member_count = guild['member_count'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass @@ -509,6 +632,7 @@ def _from_data(self, guild: GuildPayload) -> None: self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250 self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') + self._incidents_data: Optional[IncidentData] = guild.get('incidents_data') if 'channels' in guild: channels = guild['channels'] @@ -530,10 +654,11 @@ def _from_data(self, guild: GuildPayload) -> None: empty_tuple = () for presence in guild.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state) + member = self.get_member(raw_presence.user_id) + if member is not None: - member._presence_update(presence, empty_tuple) # type: ignore + member._presence_update(raw_presence, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] @@ -550,6 +675,11 @@ def _from_data(self, guild: GuildPayload) -> None: scheduled_event = ScheduledEvent(data=s, state=self._state) self._scheduled_events[scheduled_event.id] = scheduled_event + if 'soundboard_sounds' in guild: + for s in guild['soundboard_sounds']: + soundboard_sound = SoundboardSound(guild=self, data=s, state=self._state) + self._add_soundboard_sound(soundboard_sound) + @property def channels(self) -> Sequence[GuildChannel]: """Sequence[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" @@ -999,6 +1129,37 @@ def get_scheduled_event(self, scheduled_event_id: int, /) -> Optional[ScheduledE """ return self._scheduled_events.get(scheduled_event_id) + @property + def soundboard_sounds(self) -> Sequence[SoundboardSound]: + """Sequence[:class:`SoundboardSound`]: Returns a sequence of the guild's soundboard sounds. + + .. versionadded:: 2.5 + """ + return utils.SequenceProxy(self._soundboard_sounds.values()) + + def get_soundboard_sound(self, sound_id: int, /) -> Optional[SoundboardSound]: + """Returns a soundboard sound with the given ID. + + .. versionadded:: 2.5 + + Parameters + ----------- + sound_id: :class:`int` + The ID to search for. + + Returns + -------- + Optional[:class:`SoundboardSound`] + The soundboard sound or ``None`` if not found. + """ + return self._soundboard_sounds.get(sound_id) + + def _resolve_soundboard_sound(self, id: Optional[int], /) -> Optional[SoundboardSound]: + if id is None: + return + + return self._soundboard_sounds.get(id) + @property def owner(self) -> Optional[Member]: """Optional[:class:`Member`]: The member that owns the guild.""" @@ -1129,94 +1290,86 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, TextChannelPayload]: - ... + ) -> Coroutine[Any, Any, TextChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, VoiceChannelPayload]: - ... + ) -> Coroutine[Any, Any, VoiceChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.stage_voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, StageChannelPayload]: - ... + ) -> Coroutine[Any, Any, StageChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.category], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, CategoryChannelPayload]: - ... + ) -> Coroutine[Any, Any, CategoryChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.news], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, NewsChannelPayload]: - ... + ) -> Coroutine[Any, Any, NewsChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.news, ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: - ... + ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: ... @overload def _create_channel( self, name: str, channel_type: Literal[ChannelType.forum], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, ForumChannelPayload]: - ... + ) -> Coroutine[Any, Any, ForumChannelPayload]: ... @overload def _create_channel( self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, GuildChannelPayload]: - ... + ) -> Coroutine[Any, Any, GuildChannelPayload]: ... def _create_channel( self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, category: Optional[Snowflake] = None, **options: Any, ) -> Coroutine[Any, Any, GuildChannelPayload]: @@ -1256,7 +1409,7 @@ async def create_text_channel( topic: str = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, ) -> TextChannel: @@ -1398,7 +1551,7 @@ async def create_voice_channel( user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> VoiceChannel: """|coro| @@ -1491,7 +1644,7 @@ async def create_stage_channel( user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> StageChannel: """|coro| @@ -1584,7 +1737,7 @@ async def create_category( self, name: str, *, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, position: int = MISSING, ) -> CategoryChannel: @@ -1639,7 +1792,7 @@ async def create_forum( category: Optional[CategoryChannel] = None, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, @@ -1812,6 +1965,77 @@ async def delete(self) -> None: await self._state.http.delete_guild(self.id) + async def edit_me( + self, + *, + nick: Optional[str] = MISSING, + banner: Optional[bytes] = MISSING, + avatar: Optional[bytes] = MISSING, + bio: Optional[str] = MISSING, + ) -> Optional[Member]: + """|coro| + + Edits the current client member profile. + + .. note:: + To upload an avatar, or banner, a :term:`py:bytes-like object` must be passed in that + represents the image being uploaded. If this is done through a file + then the file must be opened via ``open('filename`, 'rb')`` and + the :term:`py:bytes-like object` is given through the use of ``fp.read()``. + + Parameters + ----------- + nick: Optional[:class:`str`] + The nickname you wish to change to. + Could be ``None`` to denote no nick change. + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + bio: Optional[:class:`str`] + The about me you wish to change to. + Could be ``None`` to denote no about me change. + + Returns + ----------- + Optional[:class:`Member`] + The newly modified client member. + """ + + payload: Dict[str, Any] = {} + + if nick is not MISSING: + if nick: + payload['nick'] = nick + else: + payload['nick'] = None + + if banner is not MISSING: + if banner: + payload['banner'] = utils._bytes_to_base64_data(data=banner) + else: + payload['banner'] = None + + if avatar is not MISSING: + if avatar: + payload['avatar'] = utils._bytes_to_base64_data(data=avatar) + else: + payload['avatar'] = None + + if bio is not MISSING: + if bio: + payload['bio'] = bio + else: + payload['bio'] = None + + data = await self._state.http.edit_self(guild_id=self.id, **payload) + if payload: + return Member(data=data, guild=self, state=self._state) + async def edit( self, *, @@ -1843,6 +2067,8 @@ async def edit( mfa_level: MFALevel = MISSING, raid_alerts_disabled: bool = MISSING, safety_alerts_channel: TextChannel = MISSING, + invites_disabled_until: datetime.datetime = MISSING, + dms_disabled_until: datetime.datetime = MISSING, ) -> Guild: r"""|coro| @@ -1969,6 +2195,18 @@ async def edit( .. versionadded:: 2.3 + invites_disabled_until: Optional[:class:`datetime.datetime`] + The time when invites should be enabled again, or ``None`` to disable the action. + This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. + + .. versionadded:: 2.4 + + dms_disabled_until: Optional[:class:`datetime.datetime`] + The time when direct messages should be allowed again, or ``None`` to disable the action. + This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. + + .. versionadded:: 2.4 + Raises ------- Forbidden @@ -2157,6 +2395,30 @@ async def edit( await http.edit_guild_mfa_level(self.id, mfa_level=mfa_level.value) + incident_actions_payload: IncidentData = {} + if invites_disabled_until is not MISSING: + if invites_disabled_until is None: + incident_actions_payload['invites_disabled_until'] = None + else: + if invites_disabled_until.tzinfo is None: + raise TypeError( + 'invites_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + incident_actions_payload['invites_disabled_until'] = invites_disabled_until.isoformat() + + if dms_disabled_until is not MISSING: + if dms_disabled_until is None: + incident_actions_payload['dms_disabled_until'] = None + else: + if dms_disabled_until.tzinfo is None: + raise TypeError( + 'dms_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + incident_actions_payload['dms_disabled_until'] = dms_disabled_until.isoformat() + + if incident_actions_payload: + await http.edit_incident_actions(self.id, payload=incident_actions_payload) + data = await http.edit_guild(self.id, reason=reason, **fields) return Guild(data=data, state=self._state) @@ -2524,7 +2786,7 @@ async def prune_members( The inactive members are denoted if they have not logged on in ``days`` number of days and they have no roles. - You must have :attr:`~Permissions.kick_members` to do this. + You must have both :attr:`~Permissions.kick_members` and :attr:`~Permissions.manage_guild` to do this. To check how many members you would prune without actually pruning, see the :meth:`estimate_pruned_members` function. @@ -3003,8 +3265,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3019,8 +3280,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3034,8 +3294,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3049,8 +3308,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... async def create_scheduled_event( self, @@ -3373,6 +3631,37 @@ async def fetch_roles(self) -> List[Role]: data = await self._state.http.get_roles(self.id) return [Role(guild=self, state=self._state, data=d) for d in data] + async def fetch_role(self, role_id: int, /) -> Role: + """|coro| + + Retrieves a :class:`Role` with the specified ID. + + .. versionadded:: 2.5 + + .. note:: + + This method is an API call. For general usage, consider :attr:`get_role` instead. + + Parameters + ---------- + role_id: :class:`int` + The role's ID. + + Raises + ------- + NotFound + The role requested could not be found. + HTTPException + An error occurred fetching the role. + + Returns + ------- + :class:`Role` + The retrieved role. + """ + data = await self._state.http.get_role(self.id, role_id) + return Role(guild=self, state=self._state, data=data) + @overload async def create_role( self, @@ -3384,8 +3673,7 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - ) -> Role: - ... + ) -> Role: ... @overload async def create_role( @@ -3398,8 +3686,7 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - ) -> Role: - ... + ) -> Role: ... async def create_role( self, @@ -3647,7 +3934,7 @@ async def kick(self, user: Snowflake, *, reason: Optional[str] = None) -> None: Parameters ----------- user: :class:`abc.Snowflake` - The user to kick from their guild. + The user to kick from the guild. reason: Optional[:class:`str`] The reason the user got kicked. @@ -3679,7 +3966,7 @@ async def ban( Parameters ----------- user: :class:`abc.Snowflake` - The user to ban from their guild. + The user to ban from the guild. delete_message_days: :class:`int` The number of days worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 7. @@ -3748,6 +4035,58 @@ async def unban(self, user: Snowflake, *, reason: Optional[str] = None) -> None: """ await self._state.http.unban(user.id, self.id, reason=reason) + async def bulk_ban( + self, + users: Iterable[Snowflake], + *, + reason: Optional[str] = None, + delete_message_seconds: int = 86400, + ) -> BulkBanResult: + """|coro| + + Bans multiple users from the guild. + + The users must meet the :class:`abc.Snowflake` abc. + + You must have :attr:`~Permissions.ban_members` and :attr:`~Permissions.manage_guild` to do this. + + .. versionadded:: 2.4 + + Parameters + ----------- + users: Iterable[:class:`abc.Snowflake`] + The users to ban from the guild, up to 200 users. + delete_message_seconds: :class:`int` + The number of seconds worth of messages to delete from the user + in the guild. The minimum is 0 and the maximum is 604800 (7 days). + Defaults to 1 day. + reason: Optional[:class:`str`] + The reason the users got banned. + + Raises + ------- + Forbidden + You do not have the proper permissions to ban. + HTTPException + Banning failed. + + Returns + -------- + :class:`BulkBanResult` + The result of the bulk ban operation. + """ + + response = await self._state.http.bulk_ban( + self.id, + user_ids=[u.id for u in users], + delete_message_seconds=delete_message_seconds, + reason=reason, + ) + return BulkBanResult( + banned=[Object(id=int(user_id), type=User) for user_id in response.get('banned_users', []) or []], + failed=[Object(id=int(user_id), type=User) for user_id in response.get('failed_users', []) or []], + ) + @property def vanity_url(self) -> Optional[str]: """Optional[:class:`str`]: The Discord vanity invite URL for this guild, if available. @@ -3765,7 +4104,7 @@ async def vanity_invite(self) -> Optional[Invite]: The guild must have ``VANITY_URL`` 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. Raises ------- @@ -4190,6 +4529,8 @@ async def fetch_automod_rule(self, automod_rule_id: int, /) -> AutoModRule: ------- Forbidden You do not have permission to view the automod rule. + NotFound + The automod rule does not exist within this guild. Returns -------- @@ -4287,8 +4628,221 @@ async def create_automod_rule( actions=[a.to_dict() for a in actions], enabled=enabled, exempt_roles=[str(r.id) for r in exempt_roles] if exempt_roles else None, - exempt_channel=[str(c.id) for c in exempt_channels] if exempt_channels else None, + exempt_channels=[str(c.id) for c in exempt_channels] if exempt_channels else None, reason=reason, ) return AutoModRule(data=data, guild=self, state=self._state) + + @property + def invites_paused_until(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: If invites are paused, returns when + invites will get enabled in UTC, otherwise returns None. + + .. versionadded:: 2.4 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('invites_disabled_until')) + + @property + def dms_paused_until(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: If DMs are paused, returns when DMs + will get enabled in UTC, otherwise returns None. + + .. versionadded:: 2.4 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('dms_disabled_until')) + + @property + def dm_spam_detected_at(self) -> Optional[datetime.datetime]: + """:class:`datetime.datetime`: Returns the time when DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('dm_spam_detected_at')) + + @property + def raid_detected_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('raid_detected_at')) + + def invites_paused(self) -> bool: + """:class:`bool`: Whether invites are paused in the guild. + + .. versionadded:: 2.4 + """ + if not self.invites_paused_until: + return 'INVITES_DISABLED' in self.features + + return self.invites_paused_until > utils.utcnow() + + def dms_paused(self) -> bool: + """:class:`bool`: Whether DMs are paused in the guild. + + .. versionadded:: 2.4 + """ + if not self.dms_paused_until: + return False + + return self.dms_paused_until > utils.utcnow() + + def is_dm_spam_detected(self) -> bool: + """:class:`bool`: Whether DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.dm_spam_detected_at: + return False + + return self.dm_spam_detected_at > utils.utcnow() + + def is_raid_detected(self) -> bool: + """:class:`bool`: Whether a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.raid_detected_at: + return False + + return self.raid_detected_at > utils.utcnow() + + async def fetch_soundboard_sound(self, sound_id: int, /) -> SoundboardSound: + """|coro| + + Retrieves a :class:`SoundboardSound` with the specified ID. + + .. versionadded:: 2.5 + + .. note:: + + Using this, in order to receive :attr:`SoundboardSound.user`, you must have :attr:`~Permissions.create_expressions` + or :attr:`~Permissions.manage_expressions`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`get_soundboard_sound` instead. + + Raises + ------- + NotFound + The sound requested could not be found. + HTTPException + Retrieving the sound failed. + + Returns + -------- + :class:`SoundboardSound` + The retrieved sound. + """ + data = await self._state.http.get_soundboard_sound(self.id, sound_id) + return SoundboardSound(guild=self, state=self._state, data=data) + + async def fetch_soundboard_sounds(self) -> List[SoundboardSound]: + """|coro| + + Retrieves a list of all soundboard sounds for the guild. + + .. versionadded:: 2.5 + + .. note:: + + Using this, in order to receive :attr:`SoundboardSound.user`, you must have :attr:`~Permissions.create_expressions` + or :attr:`~Permissions.manage_expressions`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`soundboard_sounds` instead. + + Raises + ------- + HTTPException + Retrieving the sounds failed. + + Returns + -------- + List[:class:`SoundboardSound`] + The retrieved soundboard sounds. + """ + data = await self._state.http.get_soundboard_sounds(self.id) + return [SoundboardSound(guild=self, state=self._state, data=sound) for sound in data['items']] + + async def create_soundboard_sound( + self, + *, + name: str, + sound: bytes, + volume: float = 1, + emoji: Optional[EmojiInputType] = None, + reason: Optional[str] = None, + ) -> SoundboardSound: + """|coro| + + Creates a :class:`SoundboardSound` for the guild. + You must have :attr:`Permissions.create_expressions` to do this. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The name of the sound. Must be between 2 and 32 characters. + sound: :class:`bytes` + The :term:`py:bytes-like object` representing the sound data. + Only MP3 and OGG sound files that don't exceed the duration of 5.2s are supported. + volume: :class:`float` + The volume of the sound. Must be between 0 and 1. Defaults to ``1``. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The emoji of the sound. + reason: Optional[:class:`str`] + The reason for creating the sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to create a soundboard sound. + HTTPException + Creating the soundboard sound failed. + + Returns + ------- + :class:`SoundboardSound` + The newly created soundboard sound. + """ + payload: Dict[str, Any] = { + 'name': name, + 'sound': utils._bytes_to_base64_data(sound, audio=True), + 'volume': volume, + 'emoji_id': None, + 'emoji_name': None, + } + + if emoji is not None: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + else: + partial_emoji = None + + if partial_emoji is not None: + if partial_emoji.id is None: + payload['emoji_name'] = partial_emoji.name + else: + payload['emoji_id'] = partial_emoji.id + + data = await self._state.http.create_soundboard_sound(self.id, reason=reason, **payload) + return SoundboardSound(guild=self, state=self._state, data=data) diff --git a/discord/http.py b/discord/http.py index 69c5c779952a..c7f043e7c71e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -48,7 +48,6 @@ from urllib.parse import quote as _uriquote from collections import deque import datetime -import socket import aiohttp @@ -58,16 +57,17 @@ from .mentions import AllowedMentions from . import __version__, utils from .utils import MISSING +from .flags import MessageFlags _log = logging.getLogger(__name__) if TYPE_CHECKING: from typing_extensions import Self - from .ui.view import View + from .ui.view import BaseView from .embeds import Embed from .message import Attachment - from .flags import MessageFlags + from .poll import Poll from .types import ( appinfo, @@ -91,8 +91,13 @@ sticker, welcome_screen, sku, + poll, + voice, + soundboard, + subscription, ) from .types.snowflake import Snowflake, SnowflakeList + from .types.gateway import SessionStartLimit from types import TracebackType @@ -145,7 +150,7 @@ def handle_message_parameters( embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, message_reference: Optional[message.MessageReference] = MISSING, stickers: Optional[SnowflakeList] = MISSING, @@ -154,6 +159,7 @@ def handle_message_parameters( thread_name: str = MISSING, channel_payload: Dict[str, Any] = MISSING, applied_tags: Optional[SnowflakeList] = MISSING, + poll: Optional[Poll] = MISSING, ) -> MultipartParameters: if files is not MISSING and file is not MISSING: raise TypeError('Cannot mix file and files keyword arguments.') @@ -187,11 +193,18 @@ def handle_message_parameters( if view is not MISSING: if view is not None: payload['components'] = view.to_components() + + if view.has_components_v2(): + if flags is not MISSING: + flags.components_v2 = True + else: + flags = MessageFlags(components_v2=True) else: payload['components'] = [] if nonce is not None: payload['nonce'] = str(nonce) + payload['enforce_nonce'] = True if message_reference is not MISSING: payload['message_reference'] = message_reference @@ -256,6 +269,9 @@ def handle_message_parameters( } payload.update(channel_payload) + if poll not in (MISSING, None): + payload['poll'] = poll._to_dict() # type: ignore + multipart = [] if files: multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) @@ -300,7 +316,7 @@ def __init__(self, method: str, path: str, *, metadata: Optional[str] = None, ** self.metadata: Optional[str] = metadata url = self.BASE + self.path if parameters: - url = url.format_map({k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()}) + url = url.format_map({k: _uriquote(v, safe='') if isinstance(v, str) else v for k, v in parameters.items()}) self.url: str = url # major parameters: @@ -769,7 +785,15 @@ async def request( raise RuntimeError('Unreachable code in HTTP handling') async def get_from_cdn(self, url: str) -> bytes: - async with self.__session.get(url) as resp: + kwargs = {} + + # Proxy support + if self.proxy is not None: + kwargs['proxy'] = self.proxy + if self.proxy_auth is not None: + kwargs['proxy_auth'] = self.proxy_auth + + async with self.__session.get(url, **kwargs) as resp: if resp.status == 200: return await resp.read() elif resp.status == 404: @@ -792,13 +816,13 @@ async def close(self) -> None: async def static_login(self, token: str) -> user.User: # Necessary to get aiohttp to stop complaining about session creation if self.connector is MISSING: - # discord does not support ipv6 - self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET) + self.connector = aiohttp.TCPConnector(limit=0) self.__session = aiohttp.ClientSession( connector=self.connector, ws_response_class=DiscordClientWebSocketResponse, trace_configs=None if self.http_trace is None else [self.http_trace], + cookie_jar=aiohttp.DummyCookieJar(), ) self._global_over = asyncio.Event() self._global_over.set() @@ -935,6 +959,7 @@ def get_reaction_users( emoji: str, limit: int, after: Optional[Snowflake] = None, + type: Optional[message.ReactionType] = None, ) -> Response[List[user.User]]: r = Route( 'GET', @@ -949,6 +974,10 @@ def get_reaction_users( } if after: params['after'] = after + + if type is not None: + params['type'] = type + return self.request(r, params=params) def clear_reactions(self, channel_id: Snowflake, message_id: Snowflake) -> Response[None]: @@ -1055,6 +1084,20 @@ def unban(self, user_id: Snowflake, guild_id: Snowflake, *, reason: Optional[str r = Route('DELETE', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, reason=reason) + def bulk_ban( + self, + guild_id: Snowflake, + user_ids: List[Snowflake], + delete_message_seconds: int = 86400, + reason: Optional[str] = None, + ) -> Response[guild.BulkBanUserResponse]: + r = Route('POST', '/guilds/{guild_id}/bulk-ban', guild_id=guild_id) + payload = { + 'user_ids': user_ids, + 'delete_message_seconds': delete_message_seconds, + } + return self.request(r, json=payload, reason=reason) + def guild_voice_state( self, user_id: Snowflake, @@ -1123,6 +1166,16 @@ def edit_member( r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, json=fields, reason=reason) + def edit_self(self, guild_id: Snowflake, **fields: Any) -> Response[member.MemberWithUser]: + r = Route(method='PATCH', path='/guilds/{guild_id}/members/@me', guild_id=guild_id) + return self.request(r, json=fields) + + def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)) + + def get_voice_state(self, guild_id: Snowflake, user_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)) + # Channel management def edit_channel( @@ -1410,6 +1463,9 @@ def get_guild(self, guild_id: Snowflake, *, with_counts: bool = True) -> Respons params = {'with_counts': int(with_counts)} return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id), params=params) + def get_guild_preview(self, guild_id: Snowflake) -> Response[guild.GuildPreview]: + return self.request(Route('GET', '/guilds/{guild_id}/preview', guild_id=guild_id)) + def delete_guild(self, guild_id: Snowflake) -> Response[None]: return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) @@ -1586,6 +1642,9 @@ def estimate_pruned_members( def get_sticker(self, sticker_id: Snowflake) -> Response[sticker.Sticker]: return self.request(Route('GET', '/stickers/{sticker_id}', sticker_id=sticker_id)) + def get_sticker_pack(self, sticker_pack_id: Snowflake) -> Response[sticker.StickerPack]: + return self.request(Route('GET', '/sticker-packs/{sticker_pack_id}', sticker_pack_id=sticker_pack_id)) + def list_premium_sticker_packs(self) -> Response[sticker.ListPremiumStickerPacks]: return self.request(Route('GET', '/sticker-packs')) @@ -1764,6 +1823,9 @@ def edit_widget( ) -> Response[widget.WidgetSettings]: return self.request(Route('PATCH', '/guilds/{guild_id}/widget', guild_id=guild_id), json=payload, reason=reason) + def edit_incident_actions(self, guild_id: Snowflake, payload: guild.IncidentData) -> Response[guild.IncidentData]: + return self.request(Route('PUT', '/guilds/{guild_id}/incident-actions', guild_id=guild_id), json=payload) + # Invite management def create_invite( @@ -1830,6 +1892,9 @@ def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Resp def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]: return self.request(Route('GET', '/guilds/{guild_id}/roles', guild_id=guild_id)) + def get_role(self, guild_id: Snowflake, role_id: Snowflake) -> Response[role.Role]: + return self.request(Route('GET', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id)) + def edit_role( self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any ) -> Response[role.Role]: @@ -1958,22 +2023,19 @@ def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: Literal[True] - ) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]: - ... + ) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]: ... @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: Literal[False] - ) -> Response[List[scheduled_event.GuildScheduledEvent]]: - ... + ) -> Response[List[scheduled_event.GuildScheduledEvent]]: ... @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: bool ) -> Union[ Response[List[scheduled_event.GuildScheduledEventWithUserCount]], Response[List[scheduled_event.GuildScheduledEvent]] - ]: - ... + ]: ... def get_scheduled_events(self, guild_id: Snowflake, with_user_count: bool) -> Response[Any]: params = {'with_user_count': int(with_user_count)} @@ -2002,20 +2064,19 @@ def create_guild_scheduled_event( @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[True] - ) -> Response[scheduled_event.GuildScheduledEventWithUserCount]: - ... + ) -> Response[scheduled_event.GuildScheduledEventWithUserCount]: ... @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[False] - ) -> Response[scheduled_event.GuildScheduledEvent]: - ... + ) -> Response[scheduled_event.GuildScheduledEvent]: ... @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool - ) -> Union[Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent]]: - ... + ) -> Union[ + Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent] + ]: ... def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool @@ -2085,8 +2146,7 @@ def get_scheduled_event_users( with_member: Literal[True], before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Response[scheduled_event.ScheduledEventUsersWithMember]: - ... + ) -> Response[scheduled_event.ScheduledEventUsersWithMember]: ... @overload def get_scheduled_event_users( @@ -2097,8 +2157,7 @@ def get_scheduled_event_users( with_member: Literal[False], before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Response[scheduled_event.ScheduledEventUsers]: - ... + ) -> Response[scheduled_event.ScheduledEventUsers]: ... @overload def get_scheduled_event_users( @@ -2109,8 +2168,7 @@ def get_scheduled_event_users( with_member: bool, before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]: - ... + ) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]: ... def get_scheduled_event_users( self, @@ -2405,6 +2463,7 @@ def get_entitlements( limit: Optional[int] = None, guild_id: Optional[Snowflake] = None, exclude_ended: Optional[bool] = None, + exclude_deleted: Optional[bool] = None, ) -> Response[List[sku.Entitlement]]: params: Dict[str, Any] = {} @@ -2422,6 +2481,8 @@ def get_entitlements( params['guild_id'] = guild_id if exclude_ended is not None: params['exclude_ended'] = int(exclude_ended) + if exclude_deleted is not None: + params['exclude_deleted'] = int(exclude_deleted) return self.request( Route('GET', '/applications/{application_id}/entitlements', application_id=application_id), params=params @@ -2437,6 +2498,16 @@ def get_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) ), ) + def consume_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) -> Response[None]: + return self.request( + Route( + 'POST', + '/applications/{application_id}/entitlements/{entitlement_id}/consume', + 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]: @@ -2465,7 +2536,78 @@ def delete_entitlement(self, application_id: Snowflake, entitlement_id: Snowflak ), ) - # Misc + # Soundboard + + def get_soundboard_default_sounds(self) -> Response[List[soundboard.SoundboardDefaultSound]]: + return self.request(Route('GET', '/soundboard-default-sounds')) + + def get_soundboard_sound(self, guild_id: Snowflake, sound_id: Snowflake) -> Response[soundboard.SoundboardSound]: + return self.request( + Route('GET', '/guilds/{guild_id}/soundboard-sounds/{sound_id}', guild_id=guild_id, sound_id=sound_id) + ) + + def get_soundboard_sounds(self, guild_id: Snowflake) -> Response[Dict[str, List[soundboard.SoundboardSound]]]: + return self.request(Route('GET', '/guilds/{guild_id}/soundboard-sounds', guild_id=guild_id)) + + def create_soundboard_sound( + self, guild_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[soundboard.SoundboardSound]: + valid_keys = ( + 'name', + 'sound', + 'volume', + 'emoji_id', + 'emoji_name', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} + + return self.request( + Route('POST', '/guilds/{guild_id}/soundboard-sounds', guild_id=guild_id), json=payload, reason=reason + ) + + def edit_soundboard_sound( + self, guild_id: Snowflake, sound_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[soundboard.SoundboardSound]: + valid_keys = ( + 'name', + 'volume', + 'emoji_id', + 'emoji_name', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys} + + return self.request( + Route( + 'PATCH', + '/guilds/{guild_id}/soundboard-sounds/{sound_id}', + guild_id=guild_id, + sound_id=sound_id, + ), + json=payload, + reason=reason, + ) + + def delete_soundboard_sound(self, guild_id: Snowflake, sound_id: Snowflake, *, reason: Optional[str]) -> Response[None]: + return self.request( + Route( + 'DELETE', + '/guilds/{guild_id}/soundboard-sounds/{sound_id}', + guild_id=guild_id, + sound_id=sound_id, + ), + reason=reason, + ) + + def send_soundboard_sound(self, channel_id: Snowflake, **payload: Any) -> Response[None]: + valid_keys = ('sound_id', 'source_guild_id') + payload = {k: v for k, v in payload.items() if k in valid_keys} + return self.request( + (Route('POST', '/channels/{channel_id}/send-soundboard-sound', channel_id=channel_id)), json=payload + ) + + # Application def application_info(self) -> Response[appinfo.AppInfo]: return self.request(Route('GET', '/oauth2/applications/@me')) @@ -2481,33 +2623,154 @@ def edit_application_info(self, *, reason: Optional[str], payload: Any) -> Respo 'cover_image', 'interactions_endpoint_url ', 'tags', + 'integration_types_config', ) payload = {k: v for k, v in payload.items() if k in valid_keys} return self.request(Route('PATCH', '/applications/@me'), json=payload, reason=reason) - async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: - try: - data = await self.request(Route('GET', '/gateway')) - except HTTPException as exc: - raise GatewayNotFound() from exc - if zlib: - value = '{0}?encoding={1}&v={2}&compress=zlib-stream' - else: - value = '{0}?encoding={1}&v={2}' - return value.format(data['url'], encoding, INTERNAL_API_VERSION) + def get_application_emojis(self, application_id: Snowflake) -> Response[appinfo.ListAppEmojis]: + return self.request(Route('GET', '/applications/{application_id}/emojis', application_id=application_id)) + + def get_application_emoji(self, application_id: Snowflake, emoji_id: Snowflake) -> Response[emoji.Emoji]: + return self.request( + Route( + 'GET', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + ) + + def create_application_emoji( + self, + application_id: Snowflake, + name: str, + image: str, + ) -> Response[emoji.Emoji]: + payload = { + 'name': name, + 'image': image, + } + + return self.request( + Route('POST', '/applications/{application_id}/emojis', application_id=application_id), json=payload + ) + + def edit_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + *, + payload: Dict[str, Any], + ) -> Response[emoji.Emoji]: + r = Route( + 'PATCH', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + return self.request(r, json=payload) + + def delete_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + ) -> Response[None]: + return self.request( + Route( + 'DELETE', + '/applications/{application_id}/emojis/{emoji_id}', + application_id=application_id, + emoji_id=emoji_id, + ) + ) + + # Poll + + def get_poll_answer_voters( + self, + channel_id: Snowflake, + message_id: Snowflake, + answer_id: Snowflake, + after: Optional[Snowflake] = None, + limit: Optional[int] = None, + ) -> Response[poll.PollAnswerVoters]: + params = {} + + if after: + params['after'] = int(after) + + if limit is not None: + params['limit'] = limit - async def get_bot_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> Tuple[int, str]: + return self.request( + Route( + 'GET', + '/channels/{channel_id}/polls/{message_id}/answers/{answer_id}', + channel_id=channel_id, + message_id=message_id, + answer_id=answer_id, + ), + params=params, + ) + + def end_poll(self, channel_id: Snowflake, message_id: Snowflake) -> Response[message.Message]: + return self.request( + Route( + 'POST', + '/channels/{channel_id}/polls/{message_id}/expire', + channel_id=channel_id, + message_id=message_id, + ) + ) + + # Subscriptions + + def list_sku_subscriptions( + self, + sku_id: Snowflake, + before: Optional[Snowflake] = None, + after: Optional[Snowflake] = None, + limit: Optional[int] = None, + user_id: Optional[Snowflake] = None, + ) -> Response[List[subscription.Subscription]]: + params = {} + + 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 user_id is not None: + params['user_id'] = user_id + + return self.request( + Route( + 'GET', + '/skus/{sku_id}/subscriptions', + sku_id=sku_id, + ), + params=params, + ) + + def get_sku_subscription(self, sku_id: Snowflake, subscription_id: Snowflake) -> Response[subscription.Subscription]: + return self.request( + Route( + 'GET', + '/skus/{sku_id}/subscriptions/{subscription_id}', + sku_id=sku_id, + subscription_id=subscription_id, + ) + ) + + # Misc + + async def get_bot_gateway(self) -> Tuple[int, str, SessionStartLimit]: try: data = await self.request(Route('GET', '/gateway/bot')) except HTTPException as exc: raise GatewayNotFound() from exc - if zlib: - value = '{0}?encoding={1}&v={2}&compress=zlib-stream' - else: - value = '{0}?encoding={1}&v={2}' - return data['shards'], value.format(data['url'], encoding, INTERNAL_API_VERSION) + return data['shards'], data['url'], data['session_start_limit'] def get_user(self, user_id: Snowflake) -> Response[user.User]: return self.request(Route('GET', '/users/{user_id}', user_id=user_id)) diff --git a/discord/interactions.py b/discord/interactions.py index 48e73855e1b0..7b0b9c493787 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -27,7 +27,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List +from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List, overload import asyncio import datetime @@ -45,6 +45,7 @@ from .permissions import Permissions from .http import handle_message_parameters from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params +from .app_commands.installs import AppCommandContext from .app_commands.namespace import Namespace from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation from .channel import _threaded_channel_factory @@ -53,6 +54,8 @@ 'Interaction', 'InteractionMessage', 'InteractionResponse', + 'InteractionCallbackResponse', + 'InteractionCallbackActivityInstance', ) if TYPE_CHECKING: @@ -60,22 +63,26 @@ Interaction as InteractionPayload, InteractionData, ApplicationCommandInteractionData, + InteractionCallback as InteractionCallbackPayload, + InteractionCallbackActivity as InteractionCallbackActivityPayload, ) from .types.webhook import ( Webhook as WebhookPayload, ) + from .types.snowflake import Snowflake from .guild import Guild from .state import ConnectionState from .file import File from .mentions import AllowedMentions from aiohttp import ClientSession from .embeds import Embed - from .ui.view import View + from .ui.view import BaseView, View, LayoutView from .app_commands.models import Choice, ChoiceT from .ui.modal import Modal from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel from .threads import Thread from .app_commands.commands import Command, ContextMenu + from .poll import Poll InteractionChannel = Union[ VoiceChannel, @@ -87,6 +94,10 @@ DMChannel, GroupChannel, ] + InteractionCallbackResource = Union[ + "InteractionMessage", + "InteractionCallbackActivityInstance", + ] MISSING: Any = utils.MISSING @@ -139,6 +150,10 @@ class Interaction(Generic[ClientT]): command_failed: :class:`bool` Whether the command associated with this interaction failed to execute. This includes checks and execution. + context: :class:`.AppCommandContext` + The context of the interaction. + + .. versionadded:: 2.4 """ __slots__: Tuple[str, ...] = ( @@ -157,6 +172,8 @@ class Interaction(Generic[ClientT]): 'command_failed', 'entitlement_sku_ids', 'entitlements', + "context", + '_integration_owners', '_permissions', '_app_permissions', '_state', @@ -183,6 +200,9 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState[ClientT]) self.command_failed: bool = False self._from_data(data) + def __repr__(self) -> str: + return f'<{self.__class__.__name__} id={self.id} type={self.type!r} guild_id={self.guild_id!r} user={self.user!r}>' + def _from_data(self, data: InteractionPayload): self.id: int = int(data['id']) self.type: InteractionType = try_enum(InteractionType, data['type']) @@ -194,17 +214,29 @@ def _from_data(self, data: InteractionPayload): 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', [])] + # This is not entirely useful currently, unsure how to expose it in a way that it is. + self._integration_owners: Dict[int, Snowflake] = { + int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items() + } + try: + value = data['context'] # pyright: ignore[reportTypedDictNotRequiredAccess] + self.context = AppCommandContext._from_value([value]) + except KeyError: + self.context = AppCommandContext() self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US')) self.guild_locale: Optional[Locale] try: - self.guild_locale = try_enum(Locale, data['guild_locale']) + self.guild_locale = try_enum(Locale, data['guild_locale']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_locale = None guild = None if self.guild_id: - guild = self._state._get_or_create_unavailable_guild(self.guild_id) + # The data type is a TypedDict but it doesn't narrow to Dict[str, Any] properly + guild = self._state._get_or_create_unavailable_guild(self.guild_id, data=data.get('guild')) # type: ignore + if guild.me is None and self._client.user is not None: + guild._add_member(Member._from_client_user(user=self._client.user, guild=guild, state=self._state)) raw_channel = data.get('channel', {}) channel_id = utils._get_as_snowflake(raw_channel, 'id') @@ -371,6 +403,22 @@ def is_expired(self) -> bool: """:class:`bool`: Returns ``True`` if the interaction is expired.""" return utils.utcnow() >= self.expires_at + def is_guild_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a guild integration. + + .. versionadded:: 2.4 + """ + if self.guild_id: + return self.guild_id == self._integration_owners.get(0) + return False + + def is_user_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a user integration. + + .. versionadded:: 2.4 + """ + return self.user.id == self._integration_owners.get(1) + async def original_response(self) -> InteractionMessage: """|coro| @@ -421,6 +469,17 @@ async def original_response(self) -> InteractionMessage: self._original_response = message return message + @overload + async def edit_original_response( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = None, + ) -> InteractionMessage: + ... + + @overload async def edit_original_response( self, *, @@ -430,6 +489,20 @@ async def edit_original_response( attachments: Sequence[Union[Attachment, File]] = MISSING, view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, + ) -> InteractionMessage: + ... + + async def edit_original_response( + self, + *, + content: Optional[str] = MISSING, + embeds: Sequence[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[BaseView] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -464,6 +537,14 @@ async def edit_original_response( view: Optional[:class:`~discord.ui.View`] The updated view to update this message with. If ``None`` is passed then the view is removed. + poll: :class:`Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted when the response type is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -493,6 +574,7 @@ async def edit_original_response( view=view, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, + poll=poll, ) as params: adapter = async_context.get() http = self._state.http @@ -585,6 +667,106 @@ async def translate( return await translator.translate(string, locale=locale, context=context) +class InteractionCallbackActivityInstance: + """Represents an activity instance launched as an interaction response. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`str` + The activity instance ID. + """ + + __slots__ = ('id',) + + def __init__(self, data: InteractionCallbackActivityPayload) -> None: + self.id: str = data['id'] + + +class InteractionCallbackResponse(Generic[ClientT]): + """Represents an interaction response callback. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The interaction ID. + type: :class:`InteractionResponseType` + The interaction callback response type. + resource: Optional[Union[:class:`InteractionMessage`, :class:`InteractionCallbackActivityInstance`]] + The resource that the interaction response created. If a message was sent, this will be + a :class:`InteractionMessage`. If an activity was launched this will be a + :class:`InteractionCallbackActivityInstance`. In any other case, this will be ``None``. + message_id: Optional[:class:`int`] + The message ID of the resource. Only available if the resource is a :class:`InteractionMessage`. + activity_id: Optional[:class:`str`] + The activity ID of the resource. Only available if the resource is a :class:`InteractionCallbackActivityInstance`. + """ + + __slots__ = ( + '_state', + '_parent', + 'type', + 'id', + '_thinking', + '_ephemeral', + 'message_id', + 'activity_id', + 'resource', + ) + + def __init__( + self, + *, + data: InteractionCallbackPayload, + parent: Interaction[ClientT], + state: ConnectionState, + type: InteractionResponseType, + ) -> None: + self._state: ConnectionState = state + self._parent: Interaction[ClientT] = parent + self.type: InteractionResponseType = type + self._update(data) + + def _update(self, data: InteractionCallbackPayload) -> None: + interaction = data['interaction'] + + self.id: int = int(interaction['id']) + self._thinking: bool = interaction.get('response_message_loading', False) + self._ephemeral: bool = interaction.get('response_message_ephemeral', False) + + self.message_id: Optional[int] = utils._get_as_snowflake(interaction, 'response_message_id') + self.activity_id: Optional[str] = interaction.get('activity_instance_id') + + self.resource: Optional[InteractionCallbackResource] = None + + resource = data.get('resource') + if resource is not None: + + self.type = try_enum(InteractionResponseType, resource['type']) + + message = resource.get('message') + activity_instance = resource.get('activity_instance') + if message is not None: + self.resource = InteractionMessage( + state=_InteractionMessageState(self._parent, self._state), # pyright: ignore[reportArgumentType] + channel=self._parent.channel, # type: ignore # channel should be the correct type here + data=message, + ) + elif activity_instance is not None: + self.resource = InteractionCallbackActivityInstance(activity_instance) + + def is_thinking(self) -> bool: + """:class:`bool`: Whether the response was a thinking defer.""" + return self._thinking + + def is_ephemeral(self) -> bool: + """:class:`bool`: Whether the response was ephemeral.""" + return self._ephemeral + + class InteractionResponse(Generic[ClientT]): """Represents a Discord interaction response. @@ -614,7 +796,12 @@ def type(self) -> Optional[InteractionResponseType]: """:class:`InteractionResponseType`: The type of response that was sent, ``None`` if response is not done.""" return self._response_type - async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> None: + async def defer( + self, + *, + ephemeral: bool = False, + thinking: bool = False, + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Defers the interaction response. @@ -628,6 +815,9 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non - :attr:`InteractionType.component` - :attr:`InteractionType.modal_submit` + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- ephemeral: :class:`bool` @@ -646,6 +836,11 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non Deferring the interaction failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallbackResponse`] + The interaction callback resource, or ``None``. """ if self._response_type: raise InteractionResponded(self._parent) @@ -670,7 +865,7 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non adapter = async_context.get() params = interaction_response_params(type=defer_type, data=data) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -679,6 +874,12 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non params=params, ) self._response_type = InteractionResponseType(defer_type) + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) async def pong(self) -> None: """|coro| @@ -712,6 +913,22 @@ async def pong(self) -> None: ) self._response_type = InteractionResponseType.pong + @overload + async def send_message( + self, + *, + file: File = MISSING, + files: Sequence[File] = MISSING, + view: LayoutView, + ephemeral: bool = False, + allowed_mentions: AllowedMentions = MISSING, + suppress_embeds: bool = False, + silent: bool = False, + delete_after: Optional[float] = None, + ) -> InteractionCallbackResponse[ClientT]: + ... + + @overload async def send_message( self, content: Optional[Any] = None, @@ -727,11 +944,34 @@ async def send_message( suppress_embeds: bool = False, silent: bool = False, delete_after: Optional[float] = None, - ) -> None: + poll: Poll = MISSING, + ) -> InteractionCallbackResponse[ClientT]: + ... + + async def send_message( + self, + content: Optional[Any] = None, + *, + embed: Embed = MISSING, + embeds: Sequence[Embed] = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + view: BaseView = MISSING, + tts: bool = False, + ephemeral: bool = False, + allowed_mentions: AllowedMentions = MISSING, + suppress_embeds: bool = False, + silent: bool = False, + delete_after: Optional[float] = None, + poll: Poll = MISSING, + ) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a message. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -748,8 +988,11 @@ async def send_message( A list of files to upload. Must be a maximum of 10. tts: :class:`bool` Indicates if the message should be sent using text-to-speech. - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] The view to send with the message. + + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. ephemeral: :class:`bool` 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 @@ -770,6 +1013,10 @@ async def send_message( then it is silently ignored. .. versionadded:: 2.1 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 Raises ------- @@ -781,6 +1028,11 @@ async def send_message( The length of ``embeds`` was invalid. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallbackResponse` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -807,10 +1059,11 @@ async def send_message( allowed_mentions=allowed_mentions, flags=flags, view=view, + poll=poll, ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -841,6 +1094,26 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + + @overload + async def edit_message( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = MISSING, + delete_after: Optional[float] = None, + suppress_embeds: bool = MISSING, + ) -> Optional[InteractionCallbackResponse[ClientT]]: + ... + + @overload async def edit_message( self, *, @@ -852,12 +1125,29 @@ async def edit_message( allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, - ) -> None: + ) -> Optional[InteractionCallbackResponse[ClientT]]: + ... + + async def edit_message( + self, + *, + content: Optional[Any] = MISSING, + embed: Optional[Embed] = MISSING, + embeds: Sequence[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[BaseView] = MISSING, + allowed_mentions: Optional[AllowedMentions] = MISSING, + delete_after: Optional[float] = None, + suppress_embeds: bool = MISSING, + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Responds to this interaction by editing the original message of a component or modal interaction. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -875,9 +1165,12 @@ async def edit_message( New files will always appear after current attachments. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. allowed_mentions: Optional[:class:`~discord.AllowedMentions`] Controls the mentions being processed in this message. See :meth:`.Message.edit` for more information. @@ -903,6 +1196,11 @@ async def edit_message( You specified both ``embed`` and ``embeds``. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallbackResponse`] + The interaction callback data, or ``None`` if editing the message was not possible. """ if self._response_type: raise InteractionResponded(self._parent) @@ -914,7 +1212,7 @@ async def edit_message( message_id = msg.id # If this was invoked via an application command then we can use its original interaction ID # Since this is used as a cache key for view updates - original_interaction_id = msg.interaction.id if msg.interaction is not None else None + original_interaction_id = msg.interaction_metadata.id if msg.interaction_metadata is not None else None else: message_id = None original_interaction_id = None @@ -945,7 +1243,7 @@ async def edit_message( ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -970,11 +1268,21 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) - async def send_modal(self, modal: Modal, /) -> None: + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + + async def send_modal(self, modal: Modal, /) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a modal. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- modal: :class:`~discord.ui.Modal` @@ -986,6 +1294,11 @@ async def send_modal(self, modal: Modal, /) -> None: Sending the modal failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallbackResponse` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -996,7 +1309,7 @@ async def send_modal(self, modal: Modal, /) -> None: http = parent._state.http params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict()) - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1008,37 +1321,12 @@ async def send_modal(self, modal: Modal, /) -> None: self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal - async def require_premium(self) -> None: - """|coro| - - Sends a message to the user prompting them that a premium purchase is required for this interaction. - - This type of response is only available for applications that have a premium SKU set up. - - Raises - ------- - HTTPException - Sending the response failed. - InteractionResponded - This interaction has already been responded to before. - """ - if self._response_type: - raise InteractionResponded(self._parent) - - parent = self._parent - adapter = async_context.get() - http = parent._state.http - - params = interaction_response_params(InteractionResponseType.premium_required.value) - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - params=params, + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, ) - self._response_type = InteractionResponseType.premium_required async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None: """|coro| @@ -1131,6 +1419,18 @@ class InteractionMessage(Message): __slots__ = () _state: _InteractionMessageState + @overload + async def edit( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = None, + delete_after: Optional[float] = None, + ) -> InteractionMessage: + ... + + @overload async def edit( self, *, @@ -1141,6 +1441,21 @@ async def edit( view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, delete_after: Optional[float] = None, + poll: Poll = MISSING, + ) -> InteractionMessage: + ... + + async def edit( + self, + *, + content: Optional[str] = MISSING, + embeds: Sequence[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[BaseView] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + delete_after: Optional[float] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -1166,15 +1481,27 @@ async def edit( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. delete_after: Optional[:class:`float`] If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, then it is silently ignored. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted if the interaction response's :attr:`InteractionResponse.type` + attribute is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -1197,8 +1524,9 @@ async def edit( embeds=embeds, embed=embed, attachments=attachments, - view=view, + view=view, # type: ignore allowed_mentions=allowed_mentions, + poll=poll, ) if delete_after is not None: await self.delete(delay=delete_after) diff --git a/discord/invite.py b/discord/invite.py index 1c18e41875d4..dd8cc954ac53 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -29,7 +29,7 @@ from .utils import parse_time, snowflake_time, _get_as_snowflake from .object import Object from .mixins import Hashable -from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, try_enum +from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, InviteType, try_enum from .appinfo import PartialAppInfo from .scheduled_event import ScheduledEvent @@ -296,6 +296,10 @@ class Invite(Hashable): Attributes ----------- + type: :class:`InviteType` + The type of the invite. + + .. versionadded: 2.4 max_age: Optional[:class:`int`] How long before the invite expires in seconds. A value of ``0`` indicates that it doesn't expire. @@ -374,6 +378,7 @@ class Invite(Hashable): 'expires_at', 'scheduled_event', 'scheduled_event_id', + 'type', ) BASE = 'https://discord.gg' @@ -387,6 +392,7 @@ def __init__( channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None, ): self._state: ConnectionState = state + self.type: InviteType = try_enum(InviteType, data.get('type', 0)) self.max_age: Optional[int] = data.get('max_age') self.code: str = data['code'] self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get('guild'), guild) @@ -431,7 +437,7 @@ def __init__( def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self: guild: Optional[Union[Guild, PartialInviteGuild]] try: - guild_data = data['guild'] + guild_data = data['guild'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: # If we're here, then this is a group DM guild = None @@ -496,7 +502,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return ( - f'' ) diff --git a/discord/member.py b/discord/member.py index 71231e426ca1..0fdc0cdaf8da 100644 --- a/discord/member.py +++ b/discord/member.py @@ -34,15 +34,15 @@ from . import utils from .asset import Asset -from .utils import MISSING -from .user import BaseUser, User, _UserTag -from .activity import create_activity, ActivityTypes +from .utils import MISSING, _bytes_to_base64_data +from .user import BaseUser, ClientUser, User, _UserTag from .permissions import Permissions -from .enums import Status, try_enum +from .enums import Status from .errors import ClientException from .colour import Colour from .object import Object from .flags import MemberFlags +from .presences import ClientStatus __all__ = ( 'VoiceState', @@ -57,17 +57,15 @@ from .channel import DMChannel, VoiceChannel, StageChannel from .flags import PublicUserFlags from .guild import Guild - from .types.activity import ( - ClientStatus as ClientStatusPayload, - PartialPresenceUpdate, - ) + from .activity import ActivityTypes + from .presences import RawPresenceUpdateEvent from .types.member import ( MemberWithUser as MemberWithUserPayload, Member as MemberPayload, UserWithMember as UserWithMemberPayload, ) from .types.gateway import GuildMemberUpdateEvent - from .types.user import User as UserPayload + from .types.user import User as UserPayload, AvatarDecorationData from .abc import Snowflake from .state import ConnectionState from .message import Message @@ -168,46 +166,6 @@ def __repr__(self) -> str: return f'<{self.__class__.__name__} {inner}>' -class _ClientStatus: - __slots__ = ('_status', 'desktop', 'mobile', 'web') - - def __init__(self): - self._status: str = 'offline' - - self.desktop: Optional[str] = None - self.mobile: Optional[str] = None - self.web: Optional[str] = None - - def __repr__(self) -> str: - attrs = [ - ('_status', self._status), - ('desktop', self.desktop), - ('mobile', self.mobile), - ('web', self.web), - ] - inner = ' '.join('%s=%r' % t for t in attrs) - return f'<{self.__class__.__name__} {inner}>' - - def _update(self, status: str, data: ClientStatusPayload, /) -> None: - self._status = status - - self.desktop = data.get('desktop') - self.mobile = data.get('mobile') - self.web = data.get('web') - - @classmethod - def _copy(cls, client_status: Self, /) -> Self: - self = cls.__new__(cls) # bypass __init__ - - self._status = client_status._status - - self.desktop = client_status.desktop - self.mobile = client_status.mobile - self.web = client_status.web - - return self - - def flatten_user(cls: T) -> T: for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): # ignore private/special methods @@ -303,9 +261,13 @@ class Member(discord.abc.Messageable, _UserTag): "Nitro boost" on the guild, if available. This could be ``None``. timed_out_until: Optional[:class:`datetime.datetime`] An aware datetime object that specifies the date and time in UTC that the member's time out will expire. - This will be set to ``None`` if the user is not timed out. + This will be set to ``None`` or a time in the past if the user is not timed out. .. versionadded:: 2.0 + client_status: :class:`ClientStatus` + Model which holds information about the status of the member on various clients/platforms via presence updates. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -318,11 +280,13 @@ class Member(discord.abc.Messageable, _UserTag): 'nick', 'timed_out_until', '_permissions', - '_client_status', + 'client_status', '_user', '_state', '_avatar', + '_banner', '_flags', + '_avatar_decoration_data', ) if TYPE_CHECKING: @@ -342,6 +306,8 @@ class Member(discord.abc.Messageable, _UserTag): banner: Optional[Asset] accent_color: Optional[Colour] accent_colour: Optional[Colour] + avatar_decoration: Optional[Asset] + avatar_decoration_sku_id: Optional[int] def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState): self._state: ConnectionState = state @@ -350,15 +316,17 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at')) self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since')) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles'])) - self._client_status: _ClientStatus = _ClientStatus() + self.client_status: ClientStatus = ClientStatus() self.activities: Tuple[ActivityTypes, ...] = () self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) self._avatar: Optional[str] = data.get('avatar') + self._banner: Optional[str] = data.get('banner') self._permissions: Optional[int] self._flags: int = data['flags'] + self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') try: - self._permissions = int(data['permissions']) + self._permissions = int(data['permissions']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self._permissions = None @@ -388,6 +356,15 @@ def _from_message(cls, *, message: Message, data: MemberPayload) -> Self: data['user'] = author._to_minimal_user_json() # type: ignore return cls(data=data, guild=message.guild, state=message._state) # type: ignore + @classmethod + def _from_client_user(cls, *, user: ClientUser, guild: Guild, state: ConnectionState) -> Self: + data = { + 'roles': [], + 'user': user._to_minimal_user_json(), + 'flags': 0, + } + return cls(data=data, guild=guild, state=state) # type: ignore + def _update_from_message(self, data: MemberPayload) -> None: self.joined_at = utils.parse_time(data.get('joined_at')) self.premium_since = utils.parse_time(data.get('premium_since')) @@ -415,7 +392,7 @@ def _copy(cls, member: Self) -> Self: self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self._client_status = _ClientStatus._copy(member._client_status) + self.client_status = member.client_status self.guild = member.guild self.nick = member.nick self.pending = member.pending @@ -425,6 +402,8 @@ def _copy(cls, member: Self) -> Self: self._permissions = member._permissions self._state = member._state self._avatar = member._avatar + self._banner = member._banner + self._avatar_decoration_data = member._avatar_decoration_data # Reference will not be copied unless necessary by PRESENCE_UPDATE # See below @@ -439,12 +418,12 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: # the nickname change is optional, # if it isn't in the payload then it didn't change try: - self.nick = data['nick'] + self.nick = data['nick'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass try: - self.pending = data['pending'] + self.pending = data['pending'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass @@ -452,19 +431,29 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self.timed_out_until = utils.parse_time(data.get('communication_disabled_until')) self._roles = utils.SnowflakeList(map(int, data['roles'])) self._avatar = data.get('avatar') + self._banner = data.get('banner') self._flags = data.get('flags', 0) + self._avatar_decoration_data = data.get('avatar_decoration_data') - def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: - self.activities = tuple(create_activity(d, self._state) for d in data['activities']) - self._client_status._update(data['status'], data['client_status']) + def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]: + self.activities = raw.activities + self.client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) - return None def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user - original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags) + original = ( + u.name, + u.discriminator, + u._avatar, + u.global_name, + u._public_flags, + u._avatar_decoration_data['sku_id'] if u._avatar_decoration_data is not None else None, + ) + + decoration_payload = user.get('avatar_decoration_data') # These keys seem to always be available modified = ( user['username'], @@ -472,17 +461,25 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: user['avatar'], user.get('global_name'), user.get('public_flags', 0), + decoration_payload['sku_id'] if decoration_payload is not None else None, ) if original != modified: to_return = User._copy(self._user) - u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified + u.name, u.discriminator, u._avatar, u.global_name, u._public_flags, u._avatar_decoration_data = ( + user['username'], + user['discriminator'], + user['avatar'], + user.get('global_name'), + user.get('public_flags', 0), + decoration_payload, + ) # Signal to dispatch on_user_update return to_return, u @property def status(self) -> Status: """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return try_enum(Status, self._client_status._status) + return self.client_status.status @property def raw_status(self) -> str: @@ -490,31 +487,36 @@ def raw_status(self) -> str: .. versionadded:: 1.5 """ - return self._client_status._status + return self.client_status._status @status.setter def status(self, value: Status) -> None: # internal use only - self._client_status._status = str(value) + self.client_status._status = str(value) @property def mobile_status(self) -> Status: """:class:`Status`: The member's status on a mobile device, if applicable.""" - return try_enum(Status, self._client_status.mobile or 'offline') + return self.client_status.mobile_status @property def desktop_status(self) -> Status: """:class:`Status`: The member's status on the desktop client, if applicable.""" - return try_enum(Status, self._client_status.desktop or 'offline') + return self.client_status.desktop_status @property def web_status(self) -> Status: """:class:`Status`: The member's status on the web client, if applicable.""" - return try_enum(Status, self._client_status.web or 'offline') + return self.client_status.web_status def is_on_mobile(self) -> bool: - """:class:`bool`: A helper function that determines if a member is active on a mobile device.""" - return self._client_status.mobile is not None + """A helper function that determines if a member is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self.client_status.is_on_mobile() @property def colour(self) -> Colour: @@ -559,7 +561,9 @@ def roles(self) -> List[Role]: role = g.get_role(role_id) if role: result.append(role) - result.append(g.default_role) + default_role = g.default_role + if default_role: + result.append(default_role) result.sort() return result @@ -617,6 +621,28 @@ def guild_avatar(self) -> Optional[Asset]: return None return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar) + @property + def display_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the member's displayed banner, if any. + + This is the member's guild banner if available, otherwise it's their + global banner. If the member has no banner set then ``None`` is returned. + + .. versionadded:: 2.5 + """ + return self.guild_banner or self._user.banner + + @property + def guild_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner + the member has. If unavailable, ``None`` is returned. + + .. versionadded:: 2.5 + """ + if self._banner is None: + return None + return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) + @property def activity(self) -> Optional[ActivityTypes]: """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary @@ -1018,7 +1044,7 @@ async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomi You must have :attr:`~Permissions.manage_roles` to use this, and the added :class:`Role`\s must appear lower in the list - of roles than the highest role of the member. + of roles than the highest role of the client. Parameters ----------- @@ -1057,7 +1083,7 @@ async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, at You must have :attr:`~Permissions.manage_roles` to use this, and the removed :class:`Role`\s must appear lower in the list - of roles than the highest role of the member. + of roles than the highest role of the client. Parameters ----------- @@ -1095,6 +1121,40 @@ async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, at for role in roles: await req(guild_id, user_id, role.id, reason=reason) + async def fetch_voice(self) -> VoiceState: + """|coro| + + Retrieves the current voice state from this member. + + .. versionadded:: 2.5 + + Raises + ------- + NotFound + The member is not in a voice channel. + Forbidden + You do not have permissions to get a voice state. + HTTPException + Retrieving the voice state failed. + + Returns + ------- + :class:`VoiceState` + The current voice state of the member. + """ + guild_id = self.guild.id + if self._state.self_id == self.id: + data = await self._state.http.get_my_voice_state(guild_id) + else: + data = await self._state.http.get_voice_state(guild_id, self.id) + + channel_id = data.get('channel_id') + channel: Optional[VocalGuildChannel] = None + if channel_id is not None: + channel = self.guild.get_channel(int(channel_id)) # type: ignore # must be voice channel here + + return VoiceState(data=data, channel=channel) + def get_role(self, role_id: int, /) -> Optional[Role]: """Returns a role with the given ID from roles which the member has. diff --git a/discord/message.py b/discord/message.py index 219d71540c53..0057b06f8737 100644 --- a/discord/message.py +++ b/discord/message.py @@ -32,6 +32,7 @@ from typing import ( Dict, TYPE_CHECKING, + Literal, Sequence, Union, List, @@ -49,20 +50,21 @@ from .reaction import Reaction from .emoji import Emoji from .partial_emoji import PartialEmoji -from .enums import InteractionType, MessageType, ChannelType, try_enum +from .enums import InteractionType, MessageReferenceType, MessageType, ChannelType, try_enum from .errors import HTTPException from .components import _component_factory from .embeds import Embed from .member import Member from .flags import MessageFlags, AttachmentFlags from .file import File -from .utils import escape_mentions, MISSING +from .utils import escape_mentions, MISSING, deprecated from .http import handle_message_parameters from .guild import Guild from .mixins import Hashable from .sticker import StickerItem, GuildSticker from .threads import Thread from .channel import PartialMessageable +from .poll import Poll if TYPE_CHECKING: from typing_extensions import Self @@ -71,9 +73,14 @@ Message as MessagePayload, Attachment as AttachmentPayload, MessageReference as MessageReferencePayload, + MessageSnapshot as MessageSnapshotPayload, MessageApplication as MessageApplicationPayload, MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, + MessageInteractionMetadata as MessageInteractionMetadataPayload, + CallMessage as CallMessagePayload, + PurchaseNotificationResponse as PurchaseNotificationResponsePayload, + GuildProductPurchase as GuildProductPurchasePayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -89,15 +96,14 @@ from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent from .abc import Snowflake from .abc import GuildChannel, MessageableChannel - from .components import ActionRow, ActionRowChildComponentType + from .components import MessageComponentType from .state import ConnectionState from .mentions import AllowedMentions from .user import User from .role import Role - from .ui.view import View + from .ui.view import BaseView, View, LayoutView EmojiInputType = Union[Emoji, PartialEmoji, str] - MessageComponentType = Union[ActionRow, ActionRowChildComponentType] __all__ = ( @@ -106,9 +112,14 @@ 'PartialMessage', 'MessageInteraction', 'MessageReference', + 'MessageSnapshot', 'DeletedReferencedMessage', 'MessageApplication', 'RoleSubscriptionInfo', + 'MessageInteractionMetadata', + 'CallMessage', + 'GuildProductPurchase', + 'PurchaseNotification', ) @@ -191,6 +202,10 @@ class Attachment(Hashable): The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -208,6 +223,7 @@ class Attachment(Hashable): 'duration', 'waveform', '_flags', + 'title', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -223,6 +239,7 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.description: Optional[str] = data.get('description') self.ephemeral: bool = data.get('ephemeral', False) self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') waveform = data.get('waveform') self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None @@ -236,11 +253,12 @@ def flags(self) -> AttachmentFlags: def is_spoiler(self) -> bool: """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.filename.startswith('SPOILER_') + # The flag is technically always present but no harm to check both + return self.filename.startswith('SPOILER_') or self.flags.spoiler def is_voice_message(self) -> bool: """:class:`bool`: Whether this attachment is a voice message.""" - return self.duration is not None and 'voice-message' in self.url + return self.duration is not None and self.waveform is not None def __repr__(self) -> str: return f'' @@ -449,6 +467,133 @@ def guild_id(self) -> Optional[int]: return self._parent.guild_id +class MessageSnapshot: + """Represents a message snapshot attached to a forwarded message. + + .. versionadded:: 2.5 + + Attributes + ----------- + type: :class:`MessageType` + The type of the forwarded message. + content: :class:`str` + The actual contents of the forwarded message. + embeds: List[:class:`Embed`] + A list of embeds the forwarded message has. + attachments: List[:class:`Attachment`] + A list of attachments given to the forwarded message. + created_at: :class:`datetime.datetime` + The forwarded message's time of creation. + flags: :class:`MessageFlags` + Extra features of the the message snapshot. + stickers: List[:class:`StickerItem`] + A list of sticker items given to the message. + components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`, :class:`Container`, :class:`SectionComponent`, :class:`TextDisplay`, :class:`MediaGalleryComponent`, :class:`FileComponent`, :class:`SeparatorComponent`, :class:`ThumbnailComponent`]] + A list of components in the message. + """ + + __slots__ = ( + '_cs_raw_channel_mentions', + '_cs_cached_message', + '_cs_raw_mentions', + '_cs_raw_role_mentions', + '_edited_timestamp', + 'attachments', + 'content', + 'embeds', + 'flags', + 'created_at', + 'type', + 'stickers', + 'components', + '_state', + ) + + @classmethod + def _from_value( + cls, + state: ConnectionState, + message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]], + ) -> List[Self]: + if not message_snapshots: + return [] + + return [cls(state, snapshot['message']) for snapshot in message_snapshots] + + def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): + self.type: MessageType = try_enum(MessageType, data['type']) + self.content: str = data['content'] + self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] + self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']] + self.created_at: datetime.datetime = utils.parse_time(data['timestamp']) + self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) + self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) + self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] + + self.components: List[MessageComponentType] = [] + for component_data in data.get('components', []): + component = _component_factory(component_data, state) # type: ignore + if component is not None: + self.components.append(component) + + self._state: ConnectionState = state + + def __repr__(self) -> str: + name = self.__class__.__name__ + return f'<{name} type={self.type!r} created_at={self.created_at!r} flags={self.flags!r}>' + + @utils.cached_slot_property('_cs_raw_mentions') + def raw_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of user IDs matched with + the syntax of ``<@user_id>`` in the message content. + + This allows you to receive the user IDs of mentioned users + even in a private message context. + """ + return [int(x) for x in re.findall(r'<@!?([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_raw_channel_mentions') + def raw_channel_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of channel IDs matched with + the syntax of ``<#channel_id>`` in the message content. + """ + return [int(x) for x in re.findall(r'<#([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_raw_role_mentions') + def raw_role_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of role IDs matched with + the syntax of ``<@&role_id>`` in the message content. + """ + return [int(x) for x in re.findall(r'<@&([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_cached_message') + def cached_message(self) -> Optional[Message]: + """Optional[:class:`Message`]: Returns the cached message this snapshot points to, if any.""" + state = self._state + return ( + utils.find( + lambda m: ( + m.created_at == self.created_at + and m.edited_at == self.edited_at + and m.content == self.content + and m.embeds == self.embeds + and m.components == self.components + and m.stickers == self.stickers + and m.attachments == self.attachments + and m.flags == self.flags + ), + reversed(state._messages), + ) + if state._messages + else None + ) + + @property + def edited_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the forwarded message.""" + return self._edited_timestamp + + class MessageReference: """Represents a reference to a :class:`~discord.Message`. @@ -459,14 +604,23 @@ class MessageReference: Attributes ----------- + type: :class:`MessageReferenceType` + The type of message reference. + + .. versionadded:: 2.5 message_id: Optional[:class:`int`] The id of the message referenced. + This can be ``None`` when this message reference was retrieved from + a system message of one of the following types: + + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` channel_id: :class:`int` The channel id of the message referenced. guild_id: Optional[:class:`int`] The guild id of the message referenced. fail_if_not_exists: :class:`bool` - Whether replying to the referenced message should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 @@ -478,15 +632,22 @@ class MessageReference: If the message was resolved at a prior point but has since been deleted then this will be of type :class:`DeletedReferencedMessage`. - Currently, this is mainly the replied to message when a user replies to a message. - .. versionadded:: 1.6 """ - __slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state') + __slots__ = ('type', 'message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state') - def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int] = None, fail_if_not_exists: bool = True): + def __init__( + self, + *, + message_id: int, + channel_id: int, + guild_id: Optional[int] = None, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ): self._state: Optional[ConnectionState] = None + self.type: MessageReferenceType = type self.resolved: Optional[Union[Message, DeletedReferencedMessage]] = None self.message_id: Optional[int] = message_id self.channel_id: int = channel_id @@ -496,6 +657,7 @@ def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int] @classmethod def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Self: self = cls.__new__(cls) + self.type = try_enum(MessageReferenceType, data.get('type', 0)) self.message_id = utils._get_as_snowflake(data, 'message_id') self.channel_id = int(data['channel_id']) self.guild_id = utils._get_as_snowflake(data, 'guild_id') @@ -505,7 +667,13 @@ def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Se return self @classmethod - def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = True) -> Self: + def from_message( + cls, + message: PartialMessage, + *, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ) -> Self: """Creates a :class:`MessageReference` from an existing :class:`~discord.Message`. .. versionadded:: 1.6 @@ -515,10 +683,14 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru message: :class:`~discord.Message` The message to be converted into a reference. fail_if_not_exists: :class:`bool` - Whether replying to the referenced message should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 + type: :class:`~discord.MessageReferenceType` + The type of message reference this is. + + .. versionadded:: 2.5 Returns ------- @@ -530,6 +702,7 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None), fail_if_not_exists=fail_if_not_exists, + type=type, ) self._state = message._state return self @@ -552,7 +725,9 @@ def __repr__(self) -> str: return f'' def to_dict(self) -> MessageReferencePayload: - result: Dict[str, Any] = {'message_id': self.message_id} if self.message_id is not None else {} + result: Dict[str, Any] = ( + {'type': self.type.value, 'message_id': self.message_id} if self.message_id is not None else {} + ) result['channel_id'] = self.channel_id if self.guild_id is not None: result['guild_id'] = self.guild_id @@ -603,7 +778,7 @@ def __init__(self, *, state: ConnectionState, guild: Optional[Guild], data: Mess self.user: Union[User, Member] = MISSING try: - payload = data['member'] + payload = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.user = state.create_user(data['user']) else: @@ -624,6 +799,155 @@ def created_at(self) -> datetime.datetime: return utils.snowflake_time(self.id) +class MessageInteractionMetadata(Hashable): + """Represents the interaction metadata of a :class:`Message` if + it was sent in response to an interaction. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two message interactions are equal. + + .. describe:: x != y + + Checks if two message interactions are not equal. + + .. describe:: hash(x) + + Returns the message interaction's hash. + + Attributes + ----------- + id: :class:`int` + The interaction ID. + type: :class:`InteractionType` + The interaction type. + user: :class:`User` + The user that invoked the interaction. + original_response_message_id: Optional[:class:`int`] + The ID of the original response message if the message is a follow-up. + interacted_message_id: Optional[:class:`int`] + The ID of the message that containes the interactive components, if applicable. + modal_interaction: Optional[:class:`.MessageInteractionMetadata`] + The metadata of the modal submit interaction that triggered this interaction, if applicable. + target_user: Optional[:class:`User`] + The user the command was run on, only applicable to user context menus. + + .. versionadded:: 2.5 + target_message_id: Optional[:class:`int`] + The ID of the message the command was run on, only applicable to message context menus. + + .. versionadded:: 2.5 + """ + + __slots__: Tuple[str, ...] = ( + 'id', + 'type', + 'user', + 'original_response_message_id', + 'interacted_message_id', + 'modal_interaction', + 'target_user', + 'target_message_id', + '_integration_owners', + '_state', + '_guild', + ) + + def __init__(self, *, state: ConnectionState, guild: Optional[Guild], data: MessageInteractionMetadataPayload) -> None: + self._guild: Optional[Guild] = guild + self._state: ConnectionState = state + + self.id: int = int(data['id']) + self.type: InteractionType = try_enum(InteractionType, data['type']) + self.user: User = state.create_user(data['user']) + self._integration_owners: Dict[int, int] = { + int(key): int(value) for key, value in data.get('authorizing_integration_owners', {}).items() + } + + self.original_response_message_id: Optional[int] = None + try: + self.original_response_message_id = int(data['original_response_message_id']) # type: ignore # EAFP + except KeyError: + pass + + self.interacted_message_id: Optional[int] = None + try: + self.interacted_message_id = int(data['interacted_message_id']) # type: ignore # EAFP + except KeyError: + pass + + self.modal_interaction: Optional[MessageInteractionMetadata] = None + try: + self.modal_interaction = MessageInteractionMetadata( + state=state, guild=guild, data=data['triggering_interaction_metadata'] # type: ignore # EAFP + ) + except KeyError: + pass + + self.target_user: Optional[User] = None + try: + self.target_user = state.create_user(data['target_user']) # type: ignore # EAFP + except KeyError: + pass + + self.target_message_id: Optional[int] = None + try: + self.target_message_id = int(data['target_message_id']) # type: ignore # EAFP + except KeyError: + pass + + def __repr__(self) -> str: + return f'' + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: The interaction's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def original_response_message(self) -> Optional[Message]: + """Optional[:class:`~discord.Message`]: The original response message if the message + is a follow-up and is found in cache. + """ + if self.original_response_message_id: + return self._state._get_message(self.original_response_message_id) + return None + + @property + def interacted_message(self) -> Optional[Message]: + """Optional[:class:`~discord.Message`]: The message that + containes the interactive components, if applicable and is found in cache. + """ + if self.interacted_message_id: + return self._state._get_message(self.interacted_message_id) + return None + + @property + def target_message(self) -> Optional[Message]: + """Optional[:class:`~discord.Message`]: The target message, if applicable and is found in cache. + + .. versionadded:: 2.5 + """ + if self.target_message_id: + return self._state._get_message(self.target_message_id) + return None + + def is_guild_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a guild integration.""" + if self._guild: + return self._guild.id == self._integration_owners.get(0) + + return False + + def is_user_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a user integration.""" + return self.user.id == self._integration_owners.get(1) + + def flatten_handlers(cls: Type[Message]) -> Type[Message]: prefix = len('_handle_') handlers = [ @@ -684,6 +1008,51 @@ def cover(self) -> Optional[Asset]: return None +class CallMessage: + """Represents a message's call data in a private channel from a :class:`~discord.Message`. + + .. versionadded:: 2.5 + + Attributes + ----------- + ended_timestamp: Optional[:class:`datetime.datetime`] + The timestamp the call has ended. + participants: List[:class:`User`] + A list of users that participated in the call. + """ + + __slots__ = ('_message', 'ended_timestamp', 'participants') + + def __repr__(self) -> str: + return f'' + + def __init__(self, *, state: ConnectionState, message: Message, data: CallMessagePayload): + self._message: Message = message + self.ended_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('ended_timestamp')) + self.participants: List[User] = [] + + for user_id in data['participants']: + user_id = int(user_id) + if user_id == self._message.author.id: + self.participants.append(self._message.author) # type: ignore # can't be a Member here + else: + user = state.get_user(user_id) + if user is not None: + self.participants.append(user) + + @property + def duration(self) -> datetime.timedelta: + """:class:`datetime.timedelta`: The duration the call has lasted or is already ongoing.""" + if self.ended_timestamp is None: + return utils.utcnow() - self._message.created_at + else: + return self.ended_timestamp - self._message.created_at + + def is_ended(self) -> bool: + """:class:`bool`: Whether the call is ended or not.""" + return self.ended_timestamp is not None + + class RoleSubscriptionInfo: """Represents a message's role subscription information. @@ -717,6 +1086,59 @@ def __init__(self, data: RoleSubscriptionDataPayload) -> None: self.is_renewal: bool = data['is_renewal'] +class GuildProductPurchase: + """Represents a message's guild product that the user has purchased. + + .. versionadded:: 2.5 + + Attributes + ----------- + listing_id: :class:`int` + The ID of the listing that the user has purchased. + product_name: :class:`str` + The name of the product that the user has purchased. + """ + + __slots__ = ('listing_id', 'product_name') + + def __init__(self, data: GuildProductPurchasePayload) -> None: + self.listing_id: int = int(data['listing_id']) + self.product_name: str = data['product_name'] + + def __hash__(self) -> int: + return self.listing_id >> 22 + + def __eq__(self, other: object) -> bool: + return isinstance(other, GuildProductPurchase) and other.listing_id == self.listing_id + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + +class PurchaseNotification: + """Represents a message's purchase notification data. + + This is currently only attached to messages of type :attr:`MessageType.purchase_notification`. + + .. versionadded:: 2.5 + + Attributes + ----------- + guild_product_purchase: Optional[:class:`GuildProductPurchase`] + The guild product purchase that prompted the message. + """ + + __slots__ = ('_type', 'guild_product_purchase') + + def __init__(self, data: PurchaseNotificationResponsePayload) -> None: + self._type: int = data['type'] + + self.guild_product_purchase: Optional[GuildProductPurchase] = None + guild_product_purchase = data.get('guild_product_purchase') + if guild_product_purchase is not None: + self.guild_product_purchase = GuildProductPurchase(guild_product_purchase) + + class PartialMessage(Hashable): """Represents a partial message to aid with working messages when only a message and channel ID are present. @@ -880,6 +1302,17 @@ async def delete(delay: float): else: await self._state.http.delete_message(self.channel.id, self.id) + @overload + async def edit( + self, + *, + view: LayoutView, + attachments: Sequence[Union[Attachment, File]] = ..., + delete_after: Optional[float] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + ) -> Message: + ... + @overload async def edit( self, @@ -915,7 +1348,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = MISSING, delete_after: Optional[float] = None, allowed_mentions: Optional[AllowedMentions] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, ) -> Message: """|coro| @@ -965,10 +1398,13 @@ async def edit( are used instead. .. versionadded:: 1.4 - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. + Raises ------- HTTPException @@ -976,6 +1412,8 @@ async def edit( Forbidden Tried to suppress a message without permissions or edited a message's content or embed that isn't yours. + NotFound + This message does not exist. TypeError You specified both ``embed`` and ``embeds`` @@ -1328,6 +1766,38 @@ async def fetch_thread(self) -> Thread: return await self.guild.fetch_channel(self.id) # type: ignore # Can only be Thread in this case + @overload + async def reply( + self, + *, + file: File = ..., + view: LayoutView, + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def reply( + self, + *, + files: Sequence[File] = ..., + view: LayoutView, + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def reply( self, @@ -1345,6 +1815,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1365,6 +1836,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1385,6 +1857,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1405,6 +1878,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1439,7 +1913,36 @@ async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: return await self.channel.send(content, reference=self, **kwargs) - def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: + async def end_poll(self) -> Message: + """|coro| + + Ends the :class:`Poll` attached to this message. + + This can only be done if you are the message author. + + If the poll was successfully ended, then it returns the updated :class:`Message`. + + Raises + ------ + ~discord.HTTPException + Ending the poll failed. + + Returns + ------- + :class:`.Message` + The updated message. + """ + + data = await self._state.http.end_poll(self.channel.id, self.id) + + return Message(state=self._state, channel=self.channel, data=data) + + def to_reference( + self, + *, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ) -> MessageReference: """Creates a :class:`~discord.MessageReference` from the current message. .. versionadded:: 1.6 @@ -1447,10 +1950,14 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: Parameters ---------- fail_if_not_exists: :class:`bool` - Whether replying using the message reference should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 + type: :class:`MessageReferenceType` + The type of message reference. + + .. versionadded:: 2.5 Returns --------- @@ -1458,7 +1965,44 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: The reference to this message. """ - return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists) + return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists, type=type) + + async def forward( + self, + destination: MessageableChannel, + *, + fail_if_not_exists: bool = True, + ) -> Message: + """|coro| + + Forwards this message to a channel. + + .. versionadded:: 2.5 + + Parameters + ---------- + destination: :class:`~discord.abc.Messageable` + The channel to forward this message to. + fail_if_not_exists: :class:`bool` + Whether replying using the message reference should raise :class:`HTTPException` + if the message no longer exists or Discord could not fetch the message. + + Raises + ------ + ~discord.HTTPException + Forwarding the message failed. + + Returns + ------- + :class:`.Message` + The message sent to the channel. + """ + reference = self.to_reference( + fail_if_not_exists=fail_if_not_exists, + type=MessageReferenceType.forward, + ) + ret = await destination.send(reference=reference) + return ret def to_message_reference_dict(self) -> MessageReferencePayload: data: MessageReferencePayload = { @@ -1517,9 +2061,16 @@ class Message(PartialMessage, Hashable): The :class:`TextChannel` or :class:`Thread` that the message was sent from. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~discord.MessageReference`] - The message that this message references. This is only applicable to messages of - type :attr:`MessageType.pins_add`, crossposted messages created by a - followed channel integration, or message replies. + The message that this message references. This is only applicable to + message replies (:attr:`MessageType.reply`), crossposted messages created by + a followed channel integration, forwarded messages, and messages of type: + + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` .. versionadded:: 1.5 @@ -1588,10 +2139,6 @@ class Message(PartialMessage, Hashable): If :attr:`Intents.message_content` is not enabled this will always be an empty list unless the bot is mentioned or the message is a direct message. - .. versionadded:: 2.0 - interaction: Optional[:class:`MessageInteraction`] - The interaction that this message is a response to. - .. versionadded:: 2.0 role_subscription: Optional[:class:`RoleSubscriptionInfo`] The data of the role subscription purchase or renewal that prompted this @@ -1610,6 +2157,26 @@ class Message(PartialMessage, Hashable): .. versionadded:: 2.2 guild: Optional[:class:`Guild`] The guild that the message belongs to, if applicable. + interaction_metadata: Optional[:class:`.MessageInteractionMetadata`] + The metadata of the interaction that this message is a response to. + + .. versionadded:: 2.4 + poll: Optional[:class:`Poll`] + The poll attached to this message. + + .. versionadded:: 2.4 + call: Optional[:class:`CallMessage`] + The call associated with this message. + + .. versionadded:: 2.5 + purchase_notification: Optional[:class:`PurchaseNotification`] + The data of the purchase notification that prompted this :attr:`MessageType.purchase_notification` message. + + .. versionadded:: 2.5 + message_snapshots: List[:class:`MessageSnapshot`] + The message snapshots attached to this message. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -1640,10 +2207,15 @@ class Message(PartialMessage, Hashable): 'activity', 'stickers', 'components', - 'interaction', + '_interaction', 'role_subscription', 'application_id', 'position', + 'interaction_metadata', + 'poll', + 'call', + 'purchase_notification', + 'message_snapshots', ) if TYPE_CHECKING: @@ -1668,20 +2240,28 @@ def __init__( self._state: ConnectionState = state self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id') self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])] - self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']] - self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] + self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data.get('attachments', [])] + self.embeds: List[Embed] = [Embed.from_dict(a) for a in data.get('embeds', [])] self.activity: Optional[MessageActivityPayload] = data.get('activity') - self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) + self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('edited_timestamp')) self.type: MessageType = try_enum(MessageType, data['type']) - self.pinned: bool = data['pinned'] + self.pinned: bool = data.get('pinned', False) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) - self.mention_everyone: bool = data['mention_everyone'] - self.tts: bool = data['tts'] + self.mention_everyone: bool = data.get('mention_everyone', False) + self.tts: bool = data.get('tts', False) self.content: str = data['content'] self.nonce: Optional[Union[int, str]] = data.get('nonce') self.position: Optional[int] = data.get('position') self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] + self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots')) + + self.poll: Optional[Poll] = None + try: + poll = data['poll'] # pyright: ignore[reportTypedDictNotRequiredAccess] + self.poll = Poll._from_data(data=poll, message=self, state=state) + except KeyError: + pass try: # if the channel doesn't have a guild attribute, we handle that @@ -1693,7 +2273,7 @@ def __init__( if self.guild is not None: try: - thread = data['thread'] + thread = data['thread'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -1704,23 +2284,32 @@ def __init__( else: self._thread = Thread(guild=self.guild, state=state, data=thread) - self.interaction: Optional[MessageInteraction] = None + self._interaction: Optional[MessageInteraction] = None + # deprecated try: - interaction = data['interaction'] + interaction = data['interaction'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: - self.interaction = MessageInteraction(state=state, guild=self.guild, data=interaction) + self._interaction = MessageInteraction(state=state, guild=self.guild, data=interaction) + self.interaction_metadata: Optional[MessageInteractionMetadata] = None try: - ref = data['message_reference'] + interaction_metadata = data['interaction_metadata'] # pyright: ignore[reportTypedDictNotRequiredAccess] + except KeyError: + pass + else: + self.interaction_metadata = MessageInteractionMetadata(state=state, guild=self.guild, data=interaction_metadata) + + try: + ref = data['message_reference'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.reference = None else: self.reference = ref = MessageReference.with_state(state, ref) try: - resolved = data['referenced_message'] + resolved = data['referenced_message'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -1738,9 +2327,16 @@ def __init__( # the channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + if self.type is MessageType.poll_result: + if isinstance(self.reference.resolved, self.__class__): + self._state._update_poll_results(self, self.reference.resolved) + else: + if self.reference.message_id: + self._state._update_poll_results(self, self.reference.message_id) + self.application: Optional[MessageApplication] = None try: - application = data['application'] + application = data['application'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -1748,15 +2344,23 @@ def __init__( self.role_subscription: Optional[RoleSubscriptionInfo] = None try: - role_subscription = data['role_subscription_data'] + role_subscription = data['role_subscription_data'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: self.role_subscription = RoleSubscriptionInfo(role_subscription) - for handler in ('author', 'member', 'mentions', 'mention_roles', 'components'): + self.purchase_notification: Optional[PurchaseNotification] = None + try: + purchase_notification = data['purchase_notification'] # pyright: ignore[reportTypedDictNotRequiredAccess] + except KeyError: + pass + else: + self.purchase_notification = PurchaseNotification(purchase_notification) + + for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: - getattr(self, f'_handle_{handler}')(data[handler]) + getattr(self, f'_handle_{handler}')(data[handler]) # type: ignore except KeyError: continue @@ -1935,7 +2539,17 @@ def _handle_components(self, data: List[ComponentPayload]) -> None: self.components.append(component) def _handle_interaction(self, data: MessageInteractionPayload): - self.interaction = MessageInteraction(state=self._state, guild=self.guild, data=data) + self._interaction = MessageInteraction(state=self._state, guild=self.guild, data=data) + + def _handle_interaction_metadata(self, data: MessageInteractionMetadataPayload): + self.interaction_metadata = MessageInteractionMetadata(state=self._state, guild=self.guild, data=data) + + def _handle_call(self, data: CallMessagePayload): + self.call: Optional[CallMessage] + if data is not None: + self.call = CallMessage(state=self._state, message=self, data=data) + else: + self.call = None def _rebind_cached_references( self, @@ -2061,6 +2675,17 @@ def thread(self) -> Optional[Thread]: # Fall back to guild threads in case one was created after the message return self._thread or self.guild.get_thread(self.id) + @property + @deprecated('interaction_metadata') + def interaction(self) -> Optional[MessageInteraction]: + """Optional[:class:`~discord.MessageInteraction`]: The interaction that this message is a response to. + + .. versionadded:: 2.0 + .. deprecated:: 2.4 + This attribute is deprecated and will be removed in a future version. Use :attr:`.interaction_metadata` instead. + """ + return self._interaction + def is_system(self) -> bool: """:class:`bool`: Whether the message is a system message. @@ -2075,6 +2700,7 @@ def is_system(self) -> bool: MessageType.chat_input_command, MessageType.context_menu_command, MessageType.thread_starter_message, + MessageType.poll_result, ) @utils.cached_slot_property('_cs_system_content') @@ -2196,10 +2822,10 @@ def system_content(self) -> str: return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!' if self.type is MessageType.role_subscription_purchase and self.role_subscription is not None: - # TODO: figure out how the message looks like for is_renewal: true total_months = self.role_subscription.total_months_subscribed months = '1 month' if total_months == 1 else f'{total_months} months' - return f'{self.author.name} joined {self.role_subscription.tier_name} and has been a subscriber of {self.guild} for {months}!' + action = 'renewed' if self.role_subscription.is_renewal else 'joined' + return f'{self.author.name} {action} **{self.role_subscription.tier_name}** and has been a subscriber of {self.guild} for {months}!' if self.type is MessageType.stage_start: return f'{self.author.name} started **{self.content}**.' @@ -2230,6 +2856,35 @@ def system_content(self) -> str: if self.type is MessageType.guild_incident_report_false_alarm: return f'{self.author.name} reported a false alarm in {self.guild}.' + if self.type is MessageType.call: + call_ended = self.call.ended_timestamp is not None # type: ignore # call can't be None here + missed = self._state.user not in self.call.participants # type: ignore # call can't be None here + + if call_ended: + duration = utils._format_call_duration(self.call.duration) # type: ignore # call can't be None here + if missed: + return 'You missed a call from {0.author.name} that lasted {1}.'.format(self, duration) + else: + return '{0.author.name} started a call that lasted {1}.'.format(self, duration) + else: + if missed: + return '{0.author.name} started a call. \N{EM DASH} Join the call'.format(self) + else: + return '{0.author.name} started a call.'.format(self) + + if self.type is MessageType.purchase_notification and self.purchase_notification is not None: + guild_product_purchase = self.purchase_notification.guild_product_purchase + if guild_product_purchase is not None: + return f'{self.author.name} has purchased {guild_product_purchase.product_name}!' + + if self.type is MessageType.poll_result: + embed = self.embeds[0] # Will always have 1 embed + poll_title = utils.get( + embed.fields, + name='poll_question_text', + ) + return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore + # Fallback for unknown message types return '' @@ -2243,7 +2898,7 @@ async def edit( suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -2257,7 +2912,7 @@ async def edit( suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -2271,7 +2926,7 @@ async def edit( suppress: bool = False, delete_after: Optional[float] = None, allowed_mentions: Optional[AllowedMentions] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, ) -> Message: """|coro| @@ -2340,6 +2995,8 @@ async def edit( Forbidden Tried to suppress a message without permissions or edited a message's content or embed that isn't yours. + NotFound + This message does not exist. TypeError You specified both ``embed`` and ``embeds`` diff --git a/discord/object.py b/discord/object.py index 2243a040836d..885ad4dc2735 100644 --- a/discord/object.py +++ b/discord/object.py @@ -102,7 +102,7 @@ def __repr__(self) -> str: return f'' def __eq__(self, other: object) -> bool: - if isinstance(other, self.type): + if isinstance(other, (self.type, self.__class__)): return self.id == other.id return NotImplemented diff --git a/discord/permissions.py b/discord/permissions.py index 5ba5ea4af7c2..b553e2578161 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -187,7 +187,7 @@ def all(cls) -> Self: permissions set to ``True``. """ # 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) + return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: @@ -208,6 +208,22 @@ def _dm_permissions(cls) -> Self: base.send_messages_in_threads = False return base + @classmethod + def _user_installed_permissions(cls, *, in_guild: bool) -> Self: + base = cls.none() + base.send_messages = True + base.attach_files = True + base.embed_links = True + base.external_emojis = True + base.send_voice_messages = True + if in_guild: + # Logically this is False but if not set to True, + # permissions just become 0. + base.read_messages = True + base.send_tts_messages = True + base.send_messages_in_threads = True + return base + @classmethod def all_channel(cls) -> Self: """A :class:`Permissions` with all channel-specific permissions set to @@ -224,6 +240,10 @@ def all_channel(cls) -> Self: - :attr:`ban_members` - :attr:`administrator` - :attr:`create_expressions` + - :attr:`moderate_members` + - :attr:`create_events` + - :attr:`manage_events` + - :attr:`view_creator_monetization_analytics` .. versionchanged:: 1.7 Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_application_commands` permissions. @@ -235,8 +255,12 @@ def all_channel(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. + + .. versionchanged:: 2.4 + Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, + :attr:`use_embedded_activities`, and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0000_0000_0100_0111_1101_1011_0011_1111_0111_1111_1111_0101_0001) + return cls(0b0000_0000_0000_0110_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) @classmethod def general(cls) -> Self: @@ -251,8 +275,11 @@ def general(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`create_expressions` permission. + + .. versionchanged:: 2.4 + Added :attr:`view_creator_monetization_analytics` permission. """ - return cls(0b0000_0000_0000_0000_0000_1000_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) + return cls(0b0000_0000_0000_0000_0000_1010_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) @classmethod def membership(cls) -> Self: @@ -278,8 +305,11 @@ def text(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`send_voice_messages` permission. + + .. versionchanged:: 2.4 + Added :attr:`send_polls` and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0000_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) + return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) @classmethod def voice(cls) -> Self: @@ -682,6 +712,14 @@ def moderate_members(self) -> int: """ return 1 << 40 + @flag_value + def view_creator_monetization_analytics(self) -> int: + """:class:`bool`: Returns ``True`` if a user can view role subscription insights. + + .. versionadded:: 2.4 + """ + return 1 << 41 + @flag_value def use_soundboard(self) -> int: """:class:`bool`: Returns ``True`` if a user can use the soundboard. @@ -722,6 +760,30 @@ def send_voice_messages(self) -> int: """ return 1 << 46 + @flag_value + def send_polls(self) -> int: + """:class:`bool`: Returns ``True`` if a user can send poll messages. + + .. versionadded:: 2.4 + """ + return 1 << 49 + + @make_permission_alias('send_polls') + def create_polls(self) -> int: + """:class:`bool`: An alias for :attr:`send_polls`. + + .. versionadded:: 2.4 + """ + return 1 << 49 + + @flag_value + def use_external_apps(self) -> int: + """:class:`bool`: Returns ``True`` if a user can use external apps. + + .. versionadded:: 2.4 + """ + return 1 << 50 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -842,6 +904,9 @@ class PermissionOverwrite: send_voice_messages: Optional[bool] create_expressions: Optional[bool] create_events: Optional[bool] + send_polls: Optional[bool] + create_polls: Optional[bool] + use_external_apps: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/player.py b/discord/player.py index 147c0628a533..bad6da88ed92 100644 --- a/discord/player.py +++ b/discord/player.py @@ -288,6 +288,12 @@ class FFmpegPCMAudio(FFmpegAudio): passed to the stdin of ffmpeg. executable: :class:`str` The executable name (and path) to use. Defaults to ``ffmpeg``. + + .. warning:: + + Since this class spawns a subprocess, care should be taken to not + pass in an arbitrary executable name when using this parameter. + pipe: :class:`bool` If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``. @@ -392,6 +398,12 @@ class FFmpegOpusAudio(FFmpegAudio): executable: :class:`str` The executable name (and path) to use. Defaults to ``ffmpeg``. + + .. warning:: + + Since this class spawns a subprocess, care should be taken to not + pass in an arbitrary executable name when using this parameter. + pipe: :class:`bool` If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``. @@ -576,22 +588,26 @@ async def probe( loop = asyncio.get_running_loop() try: codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) - except Exception: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: if not fallback: _log.exception("Probe '%s' using '%s' failed", method, executable) - return # type: ignore + return None, None _log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable) try: codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) - except Exception: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: _log.exception("Fallback probe using '%s' failed", executable) else: _log.debug("Fallback probe found codec=%s, bitrate=%s", codec, bitrate) else: _log.debug("Probe found codec=%s, bitrate=%s", codec, bitrate) - finally: - return codec, bitrate + + return codec, bitrate @staticmethod def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]: @@ -751,7 +767,8 @@ def _do_run(self) -> None: delay = max(0, self.DELAY + (next_time - time.perf_counter())) time.sleep(delay) - self.send_silence() + if client.is_connected(): + self.send_silence() def run(self) -> None: try: diff --git a/discord/poll.py b/discord/poll.py new file mode 100644 index 000000000000..6ab680abd26e --- /dev/null +++ b/discord/poll.py @@ -0,0 +1,672 @@ +""" +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, List, TYPE_CHECKING, Union, AsyncIterator, Dict + +import datetime + +from .enums import PollLayoutType, try_enum, MessageType +from . import utils +from .emoji import PartialEmoji, Emoji +from .user import User +from .object import Object +from .errors import ClientException + +if TYPE_CHECKING: + from typing_extensions import Self + + from .message import Message + from .abc import Snowflake + from .state import ConnectionState + from .member import Member + + from .types.poll import ( + PollCreate as PollCreatePayload, + PollMedia as PollMediaPayload, + PollAnswerCount as PollAnswerCountPayload, + Poll as PollPayload, + PollAnswerWithID as PollAnswerWithIDPayload, + PollResult as PollResultPayload, + PollAnswer as PollAnswerPayload, + ) + + +__all__ = ( + 'Poll', + 'PollAnswer', + 'PollMedia', +) + +MISSING = utils.MISSING +PollMediaEmoji = Union[PartialEmoji, Emoji, str] + + +class PollMedia: + """Represents the poll media for a poll item. + + .. versionadded:: 2.4 + + Attributes + ---------- + text: :class:`str` + The displayed text. + emoji: Optional[Union[:class:`PartialEmoji`, :class:`Emoji`]] + The attached emoji for this media. This is only valid for poll answers. + """ + + __slots__ = ('text', 'emoji') + + def __init__(self, /, text: str, emoji: Optional[PollMediaEmoji] = None) -> None: + self.text: str = text + self.emoji: Optional[Union[PartialEmoji, Emoji]] = PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji + + def __repr__(self) -> str: + return f'' + + def to_dict(self) -> PollMediaPayload: + payload: PollMediaPayload = {'text': self.text} + + if self.emoji is not None: + payload['emoji'] = self.emoji._to_partial().to_dict() + + return payload + + @classmethod + def from_dict(cls, *, data: PollMediaPayload) -> Self: + emoji = data.get('emoji') + + if emoji: + return cls(text=data['text'], emoji=PartialEmoji.from_dict(emoji)) + return cls(text=data['text']) + + +class PollAnswer: + """Represents a poll's answer. + + .. container:: operations + + .. describe:: str(x) + + Returns this answer's text, if any. + + .. versionadded:: 2.4 + + Attributes + ---------- + id: :class:`int` + The ID of this answer. + media: :class:`PollMedia` + The display data for this answer. + self_voted: :class:`bool` + Whether the current user has voted to this answer or not. + """ + + __slots__ = ( + 'media', + 'id', + '_state', + '_message', + '_vote_count', + 'self_voted', + '_poll', + '_victor', + ) + + def __init__( + self, + *, + message: Optional[Message], + poll: Poll, + data: PollAnswerWithIDPayload, + ) -> None: + self.media: PollMedia = PollMedia.from_dict(data=data['poll_media']) + self.id: int = int(data['answer_id']) + self._message: Optional[Message] = message + self._state: Optional[ConnectionState] = message._state if message else None + self._vote_count: int = 0 + self.self_voted: bool = False + self._poll: Poll = poll + self._victor: bool = False + + def _handle_vote_event(self, added: bool, self_voted: bool) -> None: + if added: + self._vote_count += 1 + else: + self._vote_count -= 1 + self.self_voted = self_voted + + def _update_with_results(self, payload: PollAnswerCountPayload) -> None: + self._vote_count = int(payload['count']) + self.self_voted = payload['me_voted'] + + def __str__(self) -> str: + return self.media.text + + def __repr__(self) -> str: + return f'' + + @classmethod + def from_params( + cls, + id: int, + text: str, + emoji: Optional[PollMediaEmoji] = None, + *, + poll: Poll, + message: Optional[Message], + ) -> Self: + poll_media: PollMediaPayload = {'text': text} + if emoji is not None: + emoji = PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji._to_partial() + emoji_data = emoji.to_dict() + # No need to remove animated key as it will be ignored + poll_media['emoji'] = emoji_data + + payload: PollAnswerWithIDPayload = {'answer_id': id, 'poll_media': poll_media} + + return cls(data=payload, message=message, poll=poll) + + @property + def text(self) -> str: + """:class:`str`: Returns this answer's displayed text.""" + return self.media.text + + @property + def emoji(self) -> Optional[Union[PartialEmoji, Emoji]]: + """Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]: Returns this answer's displayed + emoji, if any. + """ + return self.media.emoji + + @property + def vote_count(self) -> int: + """:class:`int`: Returns an approximate count of votes for this answer. + + If the poll is finished, the count is exact. + """ + return self._vote_count + + @property + def poll(self) -> Poll: + """:class:`Poll`: Returns the parent poll of this answer.""" + return self._poll + + def _to_dict(self) -> PollAnswerPayload: + return { + 'poll_media': self.media.to_dict(), + } + + @property + def victor(self) -> bool: + """:class:`bool`: Whether the answer is the one that had the most + votes when the poll ended. + + .. versionadded:: 2.5 + + .. note:: + + If the poll has not ended, this will always return ``False``. + """ + return self._victor + + async def voters( + self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None + ) -> AsyncIterator[Union[User, Member]]: + """Returns an :term:`asynchronous iterator` representing the users that have voted on this answer. + + The ``after`` parameter must represent a user + and meet the :class:`abc.Snowflake` abc. + + This can only be called when the parent poll was sent to a message. + + Examples + -------- + + Usage :: + + async for voter in poll_answer.voters(): + print(f'{voter} has voted for {poll_answer}!') + + Flattening into a list: :: + + voters = [voter async for voter in poll_answer.voters()] + # voters is now a list of User + + Parameters + ---------- + limit: Optional[:class:`int`] + The maximum number of results to return. + If not provided, returns all the users who + voted on this poll answer. + after: Optional[:class:`abc.Snowflake`] + For pagination, voters are sorted by member. + + Raises + ------ + HTTPException + Retrieving the users failed. + + Yields + ------ + Union[:class:`User`, :class:`Member`] + The member (if retrievable) or the user that has voted + on this poll answer. The case where it can be a :class:`Member` + is in a guild message context. Sometimes it can be a :class:`User` + if the member has left the guild or if the member is not cached. + """ + + if not self._message or not self._state: # Make type checker happy + raise ClientException('You cannot fetch users to a poll not sent with a message') + + if limit is None: + if not self._message.poll: + limit = 100 + else: + limit = self.vote_count or 100 + + while limit > 0: + retrieve = min(limit, 100) + + message = self._message + guild = self._message.guild + state = self._state + after_id = after.id if after else None + + data = await state.http.get_poll_answer_voters( + message.channel.id, message.id, self.id, after=after_id, limit=retrieve + ) + users = data['users'] + + if len(users) == 0: + # No more voters to fetch, terminate loop + break + + limit -= len(users) + after = Object(id=int(users[-1]['id'])) + + if not guild or isinstance(guild, Object): + for raw_user in reversed(users): + yield User(state=self._state, data=raw_user) + continue + + for raw_member in reversed(users): + member_id = int(raw_member['id']) + member = guild.get_member(member_id) + + yield member or User(state=self._state, data=raw_member) + + +class Poll: + """Represents a message's Poll. + + .. versionadded:: 2.4 + + Parameters + ---------- + question: Union[:class:`PollMedia`, :class:`str`] + The poll's displayed question. The text can be up to 300 characters. + duration: :class:`datetime.timedelta` + The duration of the poll. Duration must be in hours. + multiple: :class:`bool` + Whether users are allowed to select more than one answer. + Defaults to ``False``. + layout_type: :class:`PollLayoutType` + The layout type of the poll. Defaults to :attr:`PollLayoutType.default`. + + Attributes + ----------- + duration: :class:`datetime.timedelta` + The duration of the poll. + multiple: :class:`bool` + Whether users are allowed to select more than one answer. + layout_type: :class:`PollLayoutType` + The layout type of the poll. + """ + + __slots__ = ( + 'multiple', + '_answers', + 'duration', + 'layout_type', + '_question_media', + '_message', + '_expiry', + '_finalized', + '_state', + '_total_votes', + '_victor_answer_id', + ) + + def __init__( + self, + question: Union[PollMedia, str], + duration: datetime.timedelta, + *, + multiple: bool = False, + layout_type: PollLayoutType = PollLayoutType.default, + ) -> None: + self._question_media: PollMedia = PollMedia(text=question, emoji=None) if isinstance(question, str) else question + self._answers: Dict[int, PollAnswer] = {} + self.duration: datetime.timedelta = duration + + self.multiple: bool = multiple + self.layout_type: PollLayoutType = layout_type + + # NOTE: These attributes are set manually when calling + # _from_data, so it should be ``None`` now. + self._message: Optional[Message] = None + self._state: Optional[ConnectionState] = None + self._finalized: bool = False + self._expiry: Optional[datetime.datetime] = None + self._total_votes: Optional[int] = None + self._victor_answer_id: Optional[int] = None + + def _update(self, message: Message) -> None: + self._state = message._state + self._message = message + + if not message.poll: + return + + # The message's poll contains the more up to date data. + self._expiry = message.poll.expires_at + self._finalized = message.poll._finalized + self._answers = message.poll._answers + self._update_results_from_message(message) + + def _update_results_from_message(self, message: Message) -> None: + if message.type != MessageType.poll_result or not message.embeds: + return + + result_embed = message.embeds[0] # Will always have 1 embed + fields: Dict[str, str] = {field.name: field.value for field in result_embed.fields} # type: ignore + + total_votes = fields.get('total_votes') + + if total_votes is not None: + self._total_votes = int(total_votes) + + victor_answer = fields.get('victor_answer_id') + + if victor_answer is None: + return # Can't do anything else without the victor answer + + self._victor_answer_id = int(victor_answer) + + victor_answer_votes = fields['victor_answer_votes'] + + answer = self._answers[self._victor_answer_id] + answer._victor = True + answer._vote_count = int(victor_answer_votes) + self._answers[answer.id] = answer # Ensure update + + def _update_results(self, data: PollResultPayload) -> None: + self._finalized = data['is_finalized'] + + for count in data['answer_counts']: + answer = self.get_answer(int(count['id'])) + if not answer: + continue + + answer._update_with_results(count) + + def _handle_vote(self, answer_id: int, added: bool, self_voted: bool = False): + answer = self.get_answer(answer_id) + if not answer: + return + + answer._handle_vote_event(added, self_voted) + + @classmethod + def _from_data(cls, *, data: PollPayload, message: Message, state: ConnectionState) -> Self: + multiselect = data.get('allow_multiselect', False) + layout_type = try_enum(PollLayoutType, data.get('layout_type', 1)) + question_data = data.get('question') + question = question_data.get('text') + expiry = utils.parse_time(data['expiry']) # If obtained via API, then expiry is set. + # expiry - message.created_at may be a few nanos away from the actual duration + duration = datetime.timedelta(hours=round((expiry - message.created_at).total_seconds() / 3600)) + # self.created_at = message.created_at + + self = cls( + duration=duration, + multiple=multiselect, + layout_type=layout_type, + question=question, + ) + self._answers = { + int(answer['answer_id']): PollAnswer(data=answer, message=message, poll=self) for answer in data['answers'] + } + self._message = message + self._state = state + self._expiry = expiry + + try: + self._update_results(data['results']) + except KeyError: + pass + + return self + + def _to_dict(self) -> PollCreatePayload: + data: PollCreatePayload = { + 'allow_multiselect': self.multiple, + 'question': self._question_media.to_dict(), + 'duration': self.duration.total_seconds() / 3600, + 'layout_type': self.layout_type.value, + 'answers': [answer._to_dict() for answer in self.answers], + } + return data + + def __repr__(self) -> str: + return f"" + + @property + def question(self) -> str: + """:class:`str`: Returns this poll's question string.""" + return self._question_media.text + + @property + def answers(self) -> List[PollAnswer]: + """List[:class:`PollAnswer`]: Returns a read-only copy of the answers.""" + return list(self._answers.values()) + + @property + def victor_answer_id(self) -> Optional[int]: + """Optional[:class:`int`]: The victor answer ID. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + return self._victor_answer_id + + @property + def victor_answer(self) -> Optional[PollAnswer]: + """Optional[:class:`PollAnswer`]: The victor answer. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + if self.victor_answer_id is None: + return None + return self.get_answer(self.victor_answer_id) + + @property + def expires_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry. + + .. note:: + + This will **always** be ``None`` for stateless polls. + """ + return self._expiry + + @property + def created_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the poll's creation time. + + .. note:: + + This will **always** be ``None`` for stateless polls. + """ + + if not self._message: + return + return self._message.created_at + + @property + def message(self) -> Optional[Message]: + """Optional[:class:`Message`]: The message this poll is from.""" + return self._message + + @property + def total_votes(self) -> int: + """:class:`int`: Returns the sum of all the answer votes. + + If the poll has not yet finished, this is an approximate vote count. + + .. versionchanged:: 2.5 + This now returns an exact vote count when updated from its poll results message. + """ + if self._total_votes is not None: + return self._total_votes + return sum([answer.vote_count for answer in self.answers]) + + def is_finalised(self) -> bool: + """:class:`bool`: Returns whether the poll has finalised. + + This always returns ``False`` for stateless polls. + """ + return self._finalized + + is_finalized = is_finalised + + def copy(self) -> Self: + """Returns a stateless copy of this poll. + + This is meant to be used when you want to edit a stateful poll. + + Returns + ------- + :class:`Poll` + The copy of the poll. + """ + + new = self.__class__(question=self.question, duration=self.duration) + + # We want to return a stateless copy of the poll, so we should not + # override new._answers as our answers may contain a state + for answer in self.answers: + new.add_answer(text=answer.text, emoji=answer.emoji) + + return new + + def add_answer( + self, + *, + text: str, + emoji: Optional[Union[PartialEmoji, Emoji, str]] = None, + ) -> Self: + """Appends a new answer to this poll. + + Parameters + ---------- + text: :class:`str` + The text label for this poll answer. Can be up to 55 + characters. + emoji: Union[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`] + The emoji to display along the text. + + Raises + ------ + ClientException + Cannot append answers to a poll that is active. + + Returns + ------- + :class:`Poll` + This poll with the new answer appended. This allows fluent-style chaining. + """ + + if self._message: + raise ClientException('Cannot append answers to a poll that is active') + + answer = PollAnswer.from_params(id=len(self.answers) + 1, text=text, emoji=emoji, message=self._message, poll=self) + self._answers[answer.id] = answer + return self + + def get_answer( + self, + /, + id: int, + ) -> Optional[PollAnswer]: + """Returns the answer with the provided ID or ``None`` if not found. + + Parameters + ---------- + id: :class:`int` + The ID of the answer to get. + + Returns + ------- + Optional[:class:`PollAnswer`] + The answer. + """ + + return self._answers.get(id) + + async def end(self) -> Self: + """|coro| + + Ends the poll. + + Raises + ------ + ClientException + This poll has no attached message. + HTTPException + Ending the poll failed. + + Returns + ------- + :class:`Poll` + The updated poll. + """ + + if not self._message or not self._state: # Make type checker happy + raise ClientException('This poll has no attached message.') + + message = await self._message.end_poll() + self._update(message) + + return self diff --git a/discord/presences.py b/discord/presences.py new file mode 100644 index 000000000000..7fec2a09dfcc --- /dev/null +++ b/discord/presences.py @@ -0,0 +1,150 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Tuple + +from .activity import create_activity +from .enums import Status, try_enum +from .utils import MISSING, _get_as_snowflake, _RawReprMixin + +if TYPE_CHECKING: + from typing_extensions import Self + + from .activity import ActivityTypes + from .guild import Guild + from .state import ConnectionState + from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate + + +__all__ = ( + 'RawPresenceUpdateEvent', + 'ClientStatus', +) + + +class ClientStatus: + """Represents the :ddocs:`Client Status Object ` from Discord, + which holds information about the status of the user on various clients/platforms, with additional helpers. + + .. versionadded:: 2.5 + """ + + __slots__ = ('_status', 'desktop', 'mobile', 'web') + + def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None: + self._status: str = status or 'offline' + + data = data or {} + self.desktop: Optional[str] = data.get('desktop') + self.mobile: Optional[str] = data.get('mobile') + self.web: Optional[str] = data.get('web') + + def __repr__(self) -> str: + attrs = [ + ('_status', self._status), + ('desktop', self.desktop), + ('mobile', self.mobile), + ('web', self.web), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {inner}>' + + def _update(self, status: str, data: ClientStatusPayload, /) -> None: + self._status = status + + self.desktop = data.get('desktop') + self.mobile = data.get('mobile') + self.web = data.get('web') + + @classmethod + def _copy(cls, client_status: Self, /) -> Self: + self = cls.__new__(cls) # bypass __init__ + + self._status = client_status._status + + self.desktop = client_status.desktop + self.mobile = client_status.mobile + self.web = client_status.web + + return self + + @property + def status(self) -> Status: + """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" + return try_enum(Status, self._status) + + @property + def raw_status(self) -> str: + """:class:`str`: The user's overall status as a string value.""" + return self._status + + @property + def mobile_status(self) -> Status: + """:class:`Status`: The user's status on a mobile device, if applicable.""" + return try_enum(Status, self.mobile or 'offline') + + @property + def desktop_status(self) -> Status: + """:class:`Status`: The user's status on the desktop client, if applicable.""" + return try_enum(Status, self.desktop or 'offline') + + @property + def web_status(self) -> Status: + """:class:`Status`: The user's status on the web client, if applicable.""" + return try_enum(Status, self.web or 'offline') + + def is_on_mobile(self) -> bool: + """:class:`bool`: A helper function that determines if a user is active on a mobile device.""" + return self.mobile is not None + + +class RawPresenceUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_presence_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that triggered the presence update. + guild_id: Optional[:class:`int`] + The guild ID for the users presence update. Could be ``None``. + guild: Optional[:class:`Guild`] + The guild associated with the presence update and user. Could be ``None``. + client_status: :class:`ClientStatus` + The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. + activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] + The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information. + """ + + __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities') + + def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self.user_id: int = int(data['user']['id']) + self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status']) + self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities']) + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.guild: Optional[Guild] = state._get_guild(self.guild_id) diff --git a/discord/raw_models.py b/discord/raw_models.py index 556df52451ab..8304559a1ef0 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -25,14 +25,16 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union +from typing import TYPE_CHECKING, Literal, Optional, Set, List, Union -from .enums import ChannelType, try_enum -from .utils import _get_as_snowflake +from .enums import ChannelType, try_enum, ReactionType +from .utils import _get_as_snowflake, _RawReprMixin from .app_commands import AppCommandPermissions from .colour import Colour if TYPE_CHECKING: + from typing_extensions import Self + from .types.gateway import ( MessageDeleteEvent, MessageDeleteBulkEvent as BulkMessageDeleteEvent, @@ -47,6 +49,7 @@ ThreadMembersUpdate, TypingStartEvent, GuildMemberRemoveEvent, + PollVoteActionEvent, ) from .types.command import GuildApplicationCommandPermissions from .message import Message @@ -75,17 +78,10 @@ 'RawTypingEvent', 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', + 'RawPollVoteActionEvent', ) -class _RawReprMixin: - __slots__: Tuple[str, ...] = () - - def __repr__(self) -> str: - value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) - return f'<{self.__class__.__name__} {value}>' - - class RawMessageDeleteEvent(_RawReprMixin): """Represents the event payload for a :func:`on_raw_message_delete` event. @@ -108,7 +104,7 @@ def __init__(self, data: MessageDeleteEvent) -> None: self.channel_id: int = int(data['channel_id']) self.cached_message: Optional[Message] = None try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -136,7 +132,7 @@ def __init__(self, data: BulkMessageDeleteEvent) -> None: self.cached_messages: List[Message] = [] try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -158,24 +154,26 @@ class RawMessageUpdateEvent(_RawReprMixin): .. versionadded:: 1.7 data: :class:`dict` - The raw data given by the :ddocs:`gateway ` + The raw data given by the :ddocs:`gateway ` cached_message: Optional[:class:`Message`] The cached message, if found in the internal message cache. Represents the message before it is modified by the data in :attr:`RawMessageUpdateEvent.data`. + message: :class:`Message` + The updated message. + + .. versionadded:: 2.5 """ - __slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message') + __slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message', 'message') - def __init__(self, data: MessageUpdateEvent) -> None: - self.message_id: int = int(data['id']) - self.channel_id: int = int(data['channel_id']) + def __init__(self, data: MessageUpdateEvent, message: Message) -> None: + self.message_id: int = message.id + self.channel_id: int = message.channel.id self.data: MessageUpdateEvent = data + self.message: Message = message self.cached_message: Optional[Message] = None - try: - self.guild_id: Optional[int] = int(data['guild_id']) - except KeyError: - self.guild_id: Optional[int] = None + self.guild_id: Optional[int] = message.guild.id if message.guild else None class RawReactionActionEvent(_RawReprMixin): @@ -217,6 +215,10 @@ class RawReactionActionEvent(_RawReprMixin): and if ``event_type`` is ``REACTION_ADD``. .. versionadded:: 2.0 + type: :class:`ReactionType` + The type of the reaction. + + .. versionadded:: 2.4 """ __slots__ = ( @@ -230,6 +232,7 @@ class RawReactionActionEvent(_RawReprMixin): 'message_author_id', 'burst', 'burst_colours', + 'type', ) def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: ReactionActionType) -> None: @@ -242,9 +245,10 @@ def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: R 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', [])] + self.type: ReactionType = try_enum(ReactionType, data['type']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -277,7 +281,7 @@ def __init__(self, data: ReactionClearEvent) -> None: self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -307,7 +311,7 @@ def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None: self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -334,7 +338,9 @@ def __init__(self, data: IntegrationDeleteEvent) -> None: self.guild_id: int = int(data['guild_id']) try: - self.application_id: Optional[int] = int(data['application_id']) + self.application_id: Optional[int] = int( + data['application_id'] # pyright: ignore[reportTypedDictNotRequiredAccess] + ) except KeyError: self.application_id: Optional[int] = None @@ -355,7 +361,7 @@ class RawThreadUpdateEvent(_RawReprMixin): parent_id: :class:`int` The ID of the channel the thread belongs to. data: :class:`dict` - The raw data given by the :ddocs:`gateway ` + The raw data given by the :ddocs:`gateway ` thread: Optional[:class:`discord.Thread`] The thread, if it could be found in the internal cache. """ @@ -399,6 +405,20 @@ def __init__(self, data: ThreadDeleteEvent) -> None: self.parent_id: int = int(data['parent_id']) self.thread: Optional[Thread] = None + @classmethod + def _from_thread(cls, thread: Thread) -> Self: + data: ThreadDeleteEvent = { + 'id': thread.id, + 'type': thread.type.value, + 'guild_id': thread.guild.id, + 'parent_id': thread.parent_id, + } + + instance = cls(data) + instance.thread = thread + + return instance + class RawThreadMembersUpdate(_RawReprMixin): """Represents the payload for a :func:`on_raw_thread_member_remove` event. @@ -414,7 +434,7 @@ class RawThreadMembersUpdate(_RawReprMixin): member_count: :class:`int` The approximate number of members in the thread. This caps at 50. data: :class:`dict` - The raw data given by the :ddocs:`gateway `. + The raw data given by the :ddocs:`gateway `. """ __slots__ = ('thread_id', 'guild_id', 'member_count', 'data') @@ -503,3 +523,33 @@ def __init__(self, *, data: GuildApplicationCommandPermissions, state: Connectio self.permissions: List[AppCommandPermissions] = [ AppCommandPermissions(data=perm, guild=self.guild, state=state) for perm in data['permissions'] ] + + +class RawPollVoteActionEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_poll_vote_add` or :func:`on_raw_poll_vote_remove` + event. + + .. versionadded:: 2.4 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that added or removed a vote. + channel_id: :class:`int` + The channel ID where the poll vote action took place. + message_id: :class:`int` + The message ID that contains the poll the user added or removed their vote on. + guild_id: Optional[:class:`int`] + The guild ID where the vote got added or removed, if applicable.. + answer_id: :class:`int` + The poll answer's ID the user voted on. + """ + + __slots__ = ('user_id', 'channel_id', 'message_id', 'guild_id', 'answer_id') + + def __init__(self, data: PollVoteActionEvent) -> None: + self.user_id: int = int(data['user_id']) + self.channel_id: int = int(data['channel_id']) + self.message_id: int = int(data['message_id']) + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.answer_id: int = int(data['answer_id']) diff --git a/discord/reaction.py b/discord/reaction.py index cd0fbef10268..9fd933b0a57a 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -27,6 +27,7 @@ from .user import User from .object import Object +from .enums import ReactionType # fmt: off __all__ = ( @@ -185,7 +186,7 @@ async def clear(self) -> None: await self.message.clear_reaction(self.emoji) async def users( - self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None + self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None, type: Optional[ReactionType] = None ) -> AsyncIterator[Union[Member, User]]: """Returns an :term:`asynchronous iterator` representing the users that have reacted to the message. @@ -220,6 +221,11 @@ async def users( reacted to the message. after: Optional[:class:`abc.Snowflake`] For pagination, reactions are sorted by member. + type: Optional[:class:`ReactionType`] + The type of reaction to return users from. + If not provided, Discord only returns users of reactions with type ``normal``. + + .. versionadded:: 2.4 Raises -------- @@ -251,7 +257,14 @@ async def users( state = message._state after_id = after.id if after else None - data = await state.http.get_reaction_users(message.channel.id, message.id, emoji, retrieve, after=after_id) + data = await state.http.get_reaction_users( + message.channel.id, + message.id, + emoji, + retrieve, + after=after_id, + type=type.value if type is not None else None, + ) if data: limit -= len(data) diff --git a/discord/role.py b/discord/role.py index 8530d4a90a46..d7fe1e08bbe2 100644 --- a/discord/role.py +++ b/discord/role.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Union, overload, TYPE_CHECKING from .asset import Asset from .permissions import Permissions @@ -286,7 +286,7 @@ def _update(self, data: RolePayload): self._flags: int = data.get('flags', 0) try: - self.tags = RoleTags(data['tags']) + self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.tags = None @@ -522,6 +522,112 @@ async def edit( data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) + @overload + async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): + ... + + async def move( + self, + *, + beginning: bool = MISSING, + end: bool = MISSING, + above: Role = MISSING, + below: Role = MISSING, + offset: int = 0, + reason: Optional[str] = None, + ): + """|coro| + + A rich interface to help move a role relative to other roles. + + You must have :attr:`~discord.Permissions.manage_roles` to do this, + and you cannot move roles above the client's top role in the guild. + + .. versionadded:: 2.5 + + Parameters + ----------- + beginning: :class:`bool` + Whether to move this at the beginning of the role list, above the default role. + This is mutually exclusive with `end`, `above`, and `below`. + end: :class:`bool` + Whether to move this at the end of the role list. + This is mutually exclusive with `beginning`, `above`, and `below`. + above: :class:`Role` + The role that should be above our current role. + This mutually exclusive with `beginning`, `end`, and `below`. + below: :class:`Role` + The role that should be below our current role. + This mutually exclusive with `beginning`, `end`, and `above`. + offset: :class:`int` + The number of roles to offset the move by. For example, + an offset of ``2`` with ``beginning=True`` would move + it 2 above the beginning. A positive number moves it above + while a negative number moves it below. Note that this + number is relative and computed after the ``beginning``, + ``end``, ``before``, and ``after`` parameters. + reason: Optional[:class:`str`] + The reason for editing this role. Shows up on the audit log. + + Raises + ------- + Forbidden + You cannot move the role there, or lack permissions to do so. + HTTPException + Moving the role failed. + TypeError + A bad mix of arguments were passed. + ValueError + An invalid role was passed. + + Returns + -------- + List[:class:`Role`] + A list of all the roles in the guild. + """ + if sum(bool(a) for a in (beginning, end, above, below)) > 1: + raise TypeError('Only one of [beginning, end, above, below] can be used.') + + target = above or below + guild = self.guild + guild_roles = guild.roles + + if target: + if target not in guild_roles: + raise ValueError('Target role is from a different guild') + if above == guild.default_role: + raise ValueError('Role cannot be moved below the default role') + if self == target: + raise ValueError('Target role cannot be itself') + + roles = [r for r in guild_roles if r != self] + if beginning: + index = 1 + elif end: + index = len(roles) + elif above in roles: + index = roles.index(above) + elif below in roles: + index = roles.index(below) + 1 + else: + index = guild_roles.index(self) + roles.insert(max((index + offset), 1), self) + + payload: List[RolePositionUpdate] = [{'id': role.id, 'position': idx} for idx, role in enumerate(roles)] + await self._state.http.move_role_position(guild.id, payload, reason=reason) + async def delete(self, *, reason: Optional[str] = None) -> None: """|coro| diff --git a/discord/shard.py b/discord/shard.py index fc8e3380a642..454fd5e2895a 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -47,13 +47,16 @@ from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, Optional, List, Dict if TYPE_CHECKING: + from typing_extensions import Unpack from .gateway import DiscordWebSocket from .activity import BaseActivity from .flags import Intents + from .types.gateway import SessionStartLimit __all__ = ( 'AutoShardedClient', 'ShardInfo', + 'SessionStartLimits', ) _log = logging.getLogger(__name__) @@ -293,6 +296,32 @@ def is_ws_ratelimited(self) -> bool: return self._parent.ws.is_ratelimited() +class SessionStartLimits: + """A class that holds info about session start limits + + .. versionadded:: 2.5 + + Attributes + ---------- + total: :class:`int` + The total number of session starts the current user is allowed + remaining: :class:`int` + Remaining remaining number of session starts the current user is allowed + reset_after: :class:`int` + The number of milliseconds until the limit resets + max_concurrency: :class:`int` + The number of identify requests allowed per 5 seconds + """ + + __slots__ = ("total", "remaining", "reset_after", "max_concurrency") + + def __init__(self, **kwargs: Unpack[SessionStartLimit]): + self.total: int = kwargs['total'] + self.remaining: int = kwargs['remaining'] + self.reset_after: int = kwargs['reset_after'] + self.max_concurrency: int = kwargs['max_concurrency'] + + class AutoShardedClient(Client): """A client similar to :class:`Client` except it handles the complications of sharding for the user into a more manageable and transparent single @@ -415,6 +444,33 @@ def shards(self) -> Dict[int, ShardInfo]: """Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object.""" return {shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items()} + async def fetch_session_start_limits(self) -> SessionStartLimits: + """|coro| + + Get the session start limits. + + This is not typically needed, and will be handled for you by default. + + At the point where you are launching multiple instances + with manual shard ranges and are considered required to use large bot + sharding by Discord, this function when used along IPC and a + before_identity_hook can speed up session start. + + .. versionadded:: 2.5 + + Returns + ------- + :class:`SessionStartLimits` + A class containing the session start limits + + Raises + ------ + GatewayNotFound + The gateway was unreachable + """ + _, _, limits = await self.http.get_bot_gateway() + return SessionStartLimits(**limits) + async def launch_shard(self, gateway: yarl.URL, shard_id: int, *, initial: bool = False) -> None: try: coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id) @@ -434,7 +490,7 @@ async def launch_shards(self) -> None: if self.shard_count is None: self.shard_count: int - self.shard_count, gateway_url = await self.http.get_bot_gateway() + self.shard_count, gateway_url, _session_start_limit = await self.http.get_bot_gateway() gateway = yarl.URL(gateway_url) else: gateway = DiscordWebSocket.DEFAULT_GATEWAY @@ -481,18 +537,21 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self.is_closed(): - return + if self._closing_task: + return await self._closing_task + + async def _close(): + await self._connection.close() - self._closed = True - await self._connection.close() + to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()] + if to_close: + await asyncio.wait(to_close) - to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()] - if to_close: - await asyncio.wait(to_close) + await self.http.close() + self.__queue.put_nowait(EventItem(EventType.clean_close, None, None)) - await self.http.close() - self.__queue.put_nowait(EventItem(EventType.clean_close, None, None)) + self._closing_task = asyncio.create_task(_close()) + await self._closing_task async def change_presence( self, diff --git a/discord/sku.py b/discord/sku.py index 0125c5734c3b..3516370b4ee1 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -25,16 +25,18 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import AsyncIterator, Optional, TYPE_CHECKING + +from datetime import datetime from . import utils -from .app_commands import MissingApplicationID from .enums import try_enum, SKUType, EntitlementType from .flags import SKUFlags +from .object import Object +from .subscription import Subscription if TYPE_CHECKING: - from datetime import datetime - + from .abc import SnowflakeTime, Snowflake from .guild import Guild from .state import ConnectionState from .types.sku import ( @@ -100,6 +102,149 @@ def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the sku's creation time in UTC.""" return utils.snowflake_time(self.id) + async def fetch_subscription(self, subscription_id: int, /) -> Subscription: + """|coro| + + Retrieves a :class:`.Subscription` with the specified ID. + + .. versionadded:: 2.5 + + Parameters + ----------- + subscription_id: :class:`int` + The subscription's ID to fetch from. + + Raises + ------- + NotFound + An subscription with this ID does not exist. + HTTPException + Fetching the subscription failed. + + Returns + -------- + :class:`.Subscription` + The subscription you requested. + """ + data = await self._state.http.get_sku_subscription(self.id, subscription_id) + return Subscription(data=data, state=self._state) + + async def subscriptions( + self, + *, + limit: Optional[int] = 50, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + user: Snowflake, + ) -> AsyncIterator[Subscription]: + """Retrieves an :term:`asynchronous iterator` of the :class:`.Subscription` that SKU has. + + .. versionadded:: 2.5 + + Examples + --------- + + Usage :: + + async for subscription in sku.subscriptions(limit=100, user=user): + print(subscription.user_id, subscription.current_period_end) + + Flattening into a list :: + + subscriptions = [subscription async for subscription in sku.subscriptions(limit=100, user=user)] + # subscriptions is now a list of Subscription... + + All parameters are optional. + + Parameters + ----------- + limit: Optional[:class:`int`] + The number of subscriptions to retrieve. If ``None``, it retrieves every subscription for this SKU. + Note, however, that this would make it a slow operation. Defaults to ``100``. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve subscriptions 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 subscriptions 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. + user: :class:`~discord.abc.Snowflake` + The user to filter by. + + Raises + ------- + HTTPException + Fetching the subscriptions failed. + TypeError + Both ``after`` and ``before`` were provided, as Discord does not + support this type of pagination. + + Yields + -------- + :class:`.Subscription` + The subscription with the SKU. + """ + + if before is not None and after is not None: + raise TypeError('subscriptions pagination does not support both before and after') + + # This endpoint paginates in ascending order. + endpoint = self._state.http.list_sku_subscriptions + + async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): + before_id = before.id if before else None + data = await endpoint(self.id, before=before_id, limit=retrieve, user_id=user.id) + + 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.id, + after=after_id, + limit=retrieve, + user_id=user.id, + ) + + 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): + before = Object(id=utils.time_snowflake(before, high=False)) + if isinstance(after, 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) < 100: + limit = 0 + + for e in data: + yield Subscription(data=e, state=self._state) + class Entitlement: """Represents an entitlement from user or guild which has been granted access to a premium offering. @@ -126,6 +271,8 @@ class Entitlement: 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 + consumed: :class:`bool` + For consumable items, whether the entitlement has been consumed. """ __slots__ = ( @@ -139,6 +286,7 @@ class Entitlement: 'starts_at', 'ends_at', 'guild_id', + 'consumed', ) def __init__(self, state: ConnectionState, data: EntitlementPayload): @@ -152,6 +300,7 @@ def __init__(self, state: ConnectionState, data: EntitlementPayload): 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') + self.consumed: bool = data.get('consumed', False) def __repr__(self) -> str: return f'' @@ -179,6 +328,21 @@ def is_expired(self) -> bool: return False return utils.utcnow() >= self.ends_at + async def consume(self) -> None: + """|coro| + + Marks a one-time purchase entitlement as consumed. + + Raises + ------- + NotFound + The entitlement could not be found. + HTTPException + Consuming the entitlement failed. + """ + + await self._state.http.consume_entitlement(self.application_id, self.id) + async def delete(self) -> None: """|coro| @@ -186,15 +350,10 @@ async def delete(self) -> None: 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) diff --git a/discord/soundboard.py b/discord/soundboard.py new file mode 100644 index 000000000000..3351aacb78ff --- /dev/null +++ b/discord/soundboard.py @@ -0,0 +1,325 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from . import utils +from .mixins import Hashable +from .partial_emoji import PartialEmoji, _EmojiTag +from .user import User +from .utils import MISSING +from .asset import Asset, AssetMixin + +if TYPE_CHECKING: + import datetime + from typing import Dict, Any + + from .types.soundboard import ( + BaseSoundboardSound as BaseSoundboardSoundPayload, + SoundboardDefaultSound as SoundboardDefaultSoundPayload, + SoundboardSound as SoundboardSoundPayload, + ) + from .state import ConnectionState + from .guild import Guild + from .message import EmojiInputType + +__all__ = ('BaseSoundboardSound', 'SoundboardDefaultSound', 'SoundboardSound') + + +class BaseSoundboardSound(Hashable, AssetMixin): + """Represents a generic Discord soundboard sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + """ + + __slots__ = ('_state', 'id', 'volume') + + def __init__(self, *, state: ConnectionState, data: BaseSoundboardSoundPayload): + self._state: ConnectionState = state + self.id: int = int(data['sound_id']) + self._update(data) + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.id == other.id + return NotImplemented + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def _update(self, data: BaseSoundboardSoundPayload): + self.volume: float = data['volume'] + + @property + def url(self) -> str: + """:class:`str`: Returns the URL of the sound.""" + return f'{Asset.BASE}/soundboard-sounds/{self.id}' + + +class SoundboardDefaultSound(BaseSoundboardSound): + """Represents a Discord soundboard default sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + name: :class:`str` + The name of the sound. + emoji: :class:`PartialEmoji` + The emoji of the sound. + """ + + __slots__ = ('name', 'emoji') + + def __init__(self, *, state: ConnectionState, data: SoundboardDefaultSoundPayload): + self.name: str = data['name'] + self.emoji: PartialEmoji = PartialEmoji(name=data['emoji_name']) + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + attrs = [ + ('id', self.id), + ('name', self.name), + ('volume', self.volume), + ('emoji', self.emoji), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + +class SoundboardSound(BaseSoundboardSound): + """Represents a Discord soundboard sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + name: :class:`str` + The name of the sound. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the sound. ``None`` if no emoji is set. + guild: :class:`Guild` + The guild in which the sound is uploaded. + available: :class:`bool` + Whether this sound is available for use. + """ + + __slots__ = ('_state', 'name', 'emoji', '_user', 'available', '_user_id', 'guild') + + def __init__(self, *, guild: Guild, state: ConnectionState, data: SoundboardSoundPayload): + super().__init__(state=state, data=data) + self.guild = guild + self._user_id = utils._get_as_snowflake(data, 'user_id') + self._user = data.get('user') + + self._update(data) + + def __repr__(self) -> str: + attrs = [ + ('id', self.id), + ('name', self.name), + ('volume', self.volume), + ('emoji', self.emoji), + ('user', self.user), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + def _update(self, data: SoundboardSoundPayload): + super()._update(data) + + self.name: str = data['name'] + self.emoji: Optional[PartialEmoji] = None + + emoji_id = utils._get_as_snowflake(data, 'emoji_id') + emoji_name = data['emoji_name'] + if emoji_id is not None or emoji_name is not None: + self.emoji = PartialEmoji(id=emoji_id, name=emoji_name) # type: ignore # emoji_name cannot be None here + + self.available: bool = data['available'] + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the snowflake's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user who uploaded the sound.""" + if self._user is None: + if self._user_id is None: + return None + return self._state.get_user(self._user_id) + return User(state=self._state, data=self._user) + + async def edit( + self, + *, + name: str = MISSING, + volume: Optional[float] = MISSING, + emoji: Optional[EmojiInputType] = MISSING, + reason: Optional[str] = None, + ): + """|coro| + + Edits the soundboard sound. + + You must have :attr:`~Permissions.manage_expressions` to edit the sound. + If the sound was created by the client, you must have either :attr:`~Permissions.manage_expressions` + or :attr:`~Permissions.create_expressions`. + + Parameters + ---------- + name: :class:`str` + The new name of the sound. Must be between 2 and 32 characters. + volume: Optional[:class:`float`] + The new volume of the sound. Must be between 0 and 1. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The new emoji of the sound. + reason: Optional[:class:`str`] + The reason for editing this sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to edit the soundboard sound. + HTTPException + Editing the soundboard sound failed. + + Returns + ------- + :class:`SoundboardSound` + The newly updated soundboard sound. + """ + + payload: Dict[str, Any] = {} + + if name is not MISSING: + payload['name'] = name + + if volume is not MISSING: + payload['volume'] = volume + + if emoji is not MISSING: + if emoji is None: + payload['emoji_id'] = None + payload['emoji_name'] = None + else: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + else: + partial_emoji = None + + if partial_emoji is not None: + if partial_emoji.id is None: + payload['emoji_name'] = partial_emoji.name + else: + payload['emoji_id'] = partial_emoji.id + + data = await self._state.http.edit_soundboard_sound(self.guild.id, self.id, reason=reason, **payload) + return SoundboardSound(guild=self.guild, state=self._state, data=data) + + async def delete(self, *, reason: Optional[str] = None) -> None: + """|coro| + + Deletes the soundboard sound. + + You must have :attr:`~Permissions.manage_expressions` to delete the sound. + If the sound was created by the client, you must have either :attr:`~Permissions.manage_expressions` + or :attr:`~Permissions.create_expressions`. + + Parameters + ----------- + reason: Optional[:class:`str`] + The reason for deleting this sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to delete the soundboard sound. + HTTPException + Deleting the soundboard sound failed. + """ + await self._state.http.delete_soundboard_sound(self.guild.id, self.id, reason=reason) diff --git a/discord/state.py b/discord/state.py index b3da4eabf7e6..37bd138a7dd9 100644 --- a/discord/state.py +++ b/discord/state.py @@ -62,6 +62,7 @@ from .channel import * from .channel import _channel_factory from .raw_models import * +from .presences import RawPresenceUpdateEvent from .member import Member from .role import Role from .enums import ChannelType, try_enum, Status @@ -70,7 +71,7 @@ from .invite import Invite from .integrations import _integration_factory from .interactions import Interaction -from .ui.view import ViewStore, View +from .ui.view import ViewStore, BaseView from .scheduled_event import ScheduledEvent from .stage_instance import StageInstance from .threads import Thread, ThreadMember @@ -78,6 +79,9 @@ from .automod import AutoModRule, AutoModAction from .audit_logs import AuditLogEntry from ._types import ClientT +from .soundboard import SoundboardSound +from .subscription import Subscription + if TYPE_CHECKING: from .abc import PrivateChannel @@ -89,6 +93,7 @@ from .ui.item import Item from .ui.dynamic import DynamicItem from .app_commands import CommandTree, Translator + from .poll import Poll from .types.automod import AutoModerationRule, AutoModerationActionExecution from .types.snowflake import Snowflake @@ -257,6 +262,10 @@ def __init__( if not intents.members or cache_flags._empty: self.store_user = self.store_user_no_intents + self.raw_presence_flag: bool = options.get('enable_raw_presences', utils.MISSING) + if self.raw_presence_flag is utils.MISSING: + self.raw_presence_flag = not intents.members and intents.presences + self.parsers: Dict[str, Callable[[Any], None]] self.parsers = parsers = {} for attr, func in inspect.getmembers(self): @@ -403,12 +412,12 @@ def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data) return sticker - def store_view(self, view: View, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None: + def store_view(self, view: BaseView, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None: if interaction_id is not None: self._view_store.remove_interaction_mapping(interaction_id) self._view_store.add_view(view, message_id) - def prevent_view_updates_for(self, message_id: int) -> Optional[View]: + def prevent_view_updates_for(self, message_id: int) -> Optional[BaseView]: return self._view_store.remove_message_tracking(message_id) def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: @@ -418,7 +427,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: self._view_store.remove_dynamic_items(*items) @property - def persistent_views(self) -> Sequence[View]: + def persistent_views(self) -> Sequence[BaseView]: return self._view_store.persistent_views @property @@ -429,8 +438,8 @@ def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]: # the keys of self._guilds are ints return self._guilds.get(guild_id) # type: ignore - def _get_or_create_unavailable_guild(self, guild_id: int) -> Guild: - return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id) + def _get_or_create_unavailable_guild(self, guild_id: int, *, data: Optional[Dict[str, Any]] = None) -> Guild: + return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id, data=data) def _add_guild(self, guild: Guild) -> None: self._guilds[guild.id] = guild @@ -454,6 +463,14 @@ def emojis(self) -> Sequence[Emoji]: def stickers(self) -> Sequence[GuildSticker]: return utils.SequenceProxy(self._stickers.values()) + @property + def soundboard_sounds(self) -> List[SoundboardSound]: + all_sounds = [] + for guild in self.guilds: + all_sounds.extend(guild.soundboard_sounds) + + return all_sounds + def get_emoji(self, emoji_id: Optional[int]) -> Optional[Emoji]: # the keys of self._emojis are ints return self._emojis.get(emoji_id) # type: ignore @@ -523,7 +540,7 @@ def _get_guild_channel( ) -> Tuple[Union[Channel, Thread], Optional[Guild]]: channel_id = int(data['channel_id']) try: - guild_id = guild_id or int(data['guild_id']) + guild_id = guild_id or int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] guild = self._get_guild(guild_id) except KeyError: channel = DMChannel._from_message(self, channel_id) @@ -533,6 +550,34 @@ def _get_guild_channel( return channel or PartialMessageable(state=self, guild_id=guild_id, id=channel_id), guild + def _update_poll_counts(self, message: Message, answer_id: int, added: bool, self_voted: bool = False) -> Optional[Poll]: + poll = message.poll + if not poll: + return + poll._handle_vote(answer_id, added, self_voted) + return poll + + def _update_poll_results(self, from_: Message, to: Union[Message, int]) -> None: + if isinstance(to, Message): + cached = self._get_message(to.id) + elif isinstance(to, int): + cached = self._get_message(to) + + if cached is None: + return + + to = cached + else: + return + + if to.poll is None: + return + + to.poll._update_results_from_message(from_) + + if cached is not None and cached.poll: + cached.poll._update_results_from_message(from_) + async def chunker( self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None ) -> None: @@ -671,23 +716,27 @@ def parse_message_delete_bulk(self, data: gw.MessageDeleteBulkEvent) -> None: self._messages.remove(msg) # type: ignore def parse_message_update(self, data: gw.MessageUpdateEvent) -> None: - raw = RawMessageUpdateEvent(data) - message = self._get_message(raw.message_id) - if message is not None: - older_message = copy.copy(message) + channel, _ = self._get_guild_channel(data) + # channel would be the correct type here + updated_message = Message(channel=channel, data=data, state=self) # type: ignore + + raw = RawMessageUpdateEvent(data=data, message=updated_message) + cached_message = self._get_message(updated_message.id) + if cached_message is not None: + older_message = copy.copy(cached_message) raw.cached_message = older_message self.dispatch('raw_message_edit', raw) - message._update(data) + cached_message._update(data) # Coerce the `after` parameter to take the new updated Member # ref: #5999 - older_message.author = message.author - self.dispatch('message_edit', older_message, message) + older_message.author = updated_message.author + self.dispatch('message_edit', older_message, updated_message) else: self.dispatch('raw_message_edit', raw) if 'components' in data: try: - entity_id = int(data['interaction']['id']) + entity_id = int(data['interaction']['id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except (KeyError, ValueError): entity_id = raw.message_id @@ -783,22 +832,24 @@ def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None: self.dispatch('interaction', interaction) def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: - guild_id = utils._get_as_snowflake(data, 'guild_id') - # guild_id won't be None here - guild = self._get_guild(guild_id) - if guild is None: - _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) + raw = RawPresenceUpdateEvent(data=data, state=self) + + if self.raw_presence_flag: + self.dispatch('raw_presence_update', raw) + + if raw.guild is None: + _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) return - user = data['user'] - member_id = int(user['id']) - member = guild.get_member(member_id) + member = raw.guild.get_member(raw.user_id) + if member is None: - _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id) + _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', raw.user_id) return old_member = Member._copy(member) - user_update = member._presence_update(data=data, user=user) + user_update = member._presence_update(raw=raw, user=data['user']) + if user_update: self.dispatch('user_update', user_update[0], user_update[1]) @@ -831,6 +882,12 @@ def parse_channel_delete(self, data: gw.ChannelDeleteEvent) -> None: guild._scheduled_events.pop(s.id) self.dispatch('scheduled_event_delete', s) + threads = guild._remove_threads_by_channel(channel_id) + + for thread in threads: + self.dispatch('thread_delete', thread) + self.dispatch('raw_thread_delete', RawThreadDeleteEvent._from_thread(thread)) + def parse_channel_update(self, data: gw.ChannelUpdateEvent) -> None: channel_type = try_enum(ChannelType, data.get('type')) channel_id = int(data['id']) @@ -878,7 +935,7 @@ def parse_channel_create(self, data: gw.ChannelCreateEvent) -> None: def parse_channel_pins_update(self, data: gw.ChannelPinsUpdateEvent) -> None: channel_id = int(data['channel_id']) try: - guild = self._get_guild(int(data['guild_id'])) + guild = self._get_guild(int(data['guild_id'])) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: guild = None channel = self._get_private_channel(channel_id) @@ -960,7 +1017,7 @@ def parse_thread_list_sync(self, data: gw.ThreadListSyncEvent) -> None: return try: - channel_ids = {int(i) for i in data['channel_ids']} + channel_ids = {int(i) for i in data['channel_ids']} # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: # If not provided, then the entire guild is being synced # So all previous thread data should be overwritten @@ -1380,8 +1437,10 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) + if member is not None: - member._presence_update(presence, user) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self) + member._presence_update(raw_presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) @@ -1494,12 +1553,8 @@ def parse_guild_scheduled_event_update(self, data: gw.GuildScheduledEventUpdateE def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None: guild = self._get_guild(int(data['guild_id'])) if guild is not None: - try: - scheduled_event = guild._scheduled_events.pop(int(data['id'])) - except KeyError: - pass - else: - self.dispatch('scheduled_event_delete', scheduled_event) + scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data)) + self.dispatch('scheduled_event_delete', scheduled_event) else: _log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id']) @@ -1541,6 +1596,63 @@ def parse_guild_scheduled_event_user_remove(self, data: gw.GuildScheduledEventUs else: _log.debug('SCHEDULED_EVENT_USER_REMOVE referencing unknown guild ID: %s. Discarding.', data['guild_id']) + def parse_guild_soundboard_sound_create(self, data: gw.GuildSoundBoardSoundCreateEvent) -> None: + guild_id = int(data['guild_id']) # type: ignore # can't be None here + guild = self._get_guild(guild_id) + if guild is not None: + sound = SoundboardSound(guild=guild, state=self, data=data) + guild._add_soundboard_sound(sound) + self.dispatch('soundboard_sound_create', sound) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_CREATE referencing unknown guild ID: %s. Discarding.', guild_id) + + def _update_and_dispatch_sound_update(self, sound: SoundboardSound, data: gw.GuildSoundBoardSoundUpdateEvent): + old_sound = copy.copy(sound) + sound._update(data) + self.dispatch('soundboard_sound_update', old_sound, sound) + + def parse_guild_soundboard_sound_update(self, data: gw.GuildSoundBoardSoundUpdateEvent) -> None: + guild_id = int(data['guild_id']) # type: ignore # can't be None here + guild = self._get_guild(guild_id) + if guild is not None: + sound_id = int(data['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + self._update_and_dispatch_sound_update(sound, data) + else: + _log.warning('GUILD_SOUNDBOARD_SOUND_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + + def parse_guild_soundboard_sound_delete(self, data: gw.GuildSoundBoardSoundDeleteEvent) -> None: + guild_id = int(data['guild_id']) + guild = self._get_guild(guild_id) + if guild is not None: + sound_id = int(data['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + guild._remove_soundboard_sound(sound) + self.dispatch('soundboard_sound_delete', sound) + else: + _log.warning('GUILD_SOUNDBOARD_SOUND_DELETE referencing unknown sound ID: %s. Discarding.', sound_id) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_DELETE referencing unknown guild ID: %s. Discarding.', guild_id) + + def parse_guild_soundboard_sounds_update(self, data: gw.GuildSoundBoardSoundsUpdateEvent) -> None: + guild_id = int(data['guild_id']) + guild = self._get_guild(guild_id) + if guild is None: + _log.debug('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + return + + for raw_sound in data['soundboard_sounds']: + sound_id = int(raw_sound['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + self._update_and_dispatch_sound_update(sound, raw_sound) + else: + _log.warning('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) + def parse_application_command_permissions_update(self, data: GuildApplicationCommandPermissionsPayload): raw = RawAppCommandPermissionsUpdateEvent(data=data, state=self) self.dispatch('raw_app_command_permissions_update', raw) @@ -1571,6 +1683,14 @@ def parse_voice_state_update(self, data: gw.VoiceStateUpdateEvent) -> None: else: _log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id']) + def parse_voice_channel_effect_send(self, data: gw.VoiceChannelEffectSendEvent): + guild = self._get_guild(int(data['guild_id'])) + if guild is not None: + effect = VoiceChannelEffect(state=self, data=data, guild=guild) + self.dispatch('voice_channel_effect', effect) + else: + _log.debug('VOICE_CHANNEL_EFFECT_SEND referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + def parse_voice_server_update(self, data: gw.VoiceServerUpdateEvent) -> None: key_id = int(data['guild_id']) @@ -1586,7 +1706,8 @@ def parse_typing_start(self, data: gw.TypingStartEvent) -> None: if channel is not None: if isinstance(channel, DMChannel): - channel.recipient = raw.user + if raw.user is not None and raw.user not in channel.recipients: + channel.recipients.append(raw.user) elif guild is not None: raw.user = guild.get_member(raw.user_id) @@ -1612,6 +1733,54 @@ def parse_entitlement_delete(self, data: gw.EntitlementDeleteEvent) -> None: entitlement = Entitlement(data=data, state=self) self.dispatch('entitlement_delete', entitlement) + def parse_message_poll_vote_add(self, data: gw.PollVoteActionEvent) -> None: + raw = RawPollVoteActionEvent(data) + + self.dispatch('raw_poll_vote_add', raw) + + message = self._get_message(raw.message_id) + guild = self._get_guild(raw.guild_id) + + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + + if message and user: + poll = self._update_poll_counts(message, raw.answer_id, True, raw.user_id == self.self_id) + if poll: + self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) + + def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: + raw = RawPollVoteActionEvent(data) + + self.dispatch('raw_poll_vote_remove', raw) + + message = self._get_message(raw.message_id) + guild = self._get_guild(raw.guild_id) + + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + + if message and user: + poll = self._update_poll_counts(message, raw.answer_id, False, raw.user_id == self.self_id) + if poll: + self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) + + def parse_subscription_create(self, data: gw.SubscriptionCreateEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_create', subscription) + + def parse_subscription_update(self, data: gw.SubscriptionUpdateEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_update', subscription) + + def parse_subscription_delete(self, data: gw.SubscriptionDeleteEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_delete', subscription) + def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): return channel.guild.get_member(user_id) @@ -1656,6 +1825,15 @@ def get_channel(self, id: Optional[int]) -> Optional[Union[Channel, Thread]]: def create_message(self, *, channel: MessageableChannel, data: MessagePayload) -> Message: return Message(state=self, channel=channel, data=data) + def get_soundboard_sound(self, id: Optional[int]) -> Optional[SoundboardSound]: + if id is None: + return + + for guild in self.guilds: + sound = guild._resolve_soundboard_sound(id) + if sound is not None: + return sound + class AutoShardedConnectionState(ConnectionState[ClientT]): def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/discord/sticker.py b/discord/sticker.py index 225e7648a167..bf90f8866526 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -28,8 +28,7 @@ from .mixins import Hashable from .asset import Asset, AssetMixin -from .utils import cached_slot_property, find, snowflake_time, get, MISSING, _get_as_snowflake -from .errors import InvalidData +from .utils import cached_slot_property, snowflake_time, get, MISSING, _get_as_snowflake from .enums import StickerType, StickerFormatType, try_enum __all__ = ( @@ -51,7 +50,6 @@ Sticker as StickerPayload, StandardSticker as StandardStickerPayload, GuildSticker as GuildStickerPayload, - ListPremiumStickerPacks as ListPremiumStickerPacksPayload, ) @@ -203,7 +201,10 @@ def __init__(self, *, state: ConnectionState, data: StickerItemPayload) -> None: self.name: str = data['name'] self.id: int = int(data['id']) self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type']) - self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' + if self.format is StickerFormatType.gif: + self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif' + else: + self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' def __repr__(self) -> str: return f'' @@ -258,8 +259,6 @@ class Sticker(_StickerTag): The id of the sticker. description: :class:`str` The description of the sticker. - pack_id: :class:`int` - The id of the sticker's pack. format: :class:`StickerFormatType` The format for the sticker's image. url: :class:`str` @@ -277,7 +276,10 @@ def _from_data(self, data: StickerPayload) -> None: self.name: str = data['name'] self.description: str = data['description'] self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type']) - self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' + if self.format is StickerFormatType.gif: + self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif' + else: + self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' def __repr__(self) -> str: return f'' @@ -349,9 +351,12 @@ async def pack(self) -> StickerPack: Retrieves the sticker pack that this sticker belongs to. + .. versionchanged:: 2.5 + Now raises ``NotFound`` instead of ``InvalidData``. + Raises -------- - InvalidData + NotFound The corresponding sticker pack was not found. HTTPException Retrieving the sticker pack failed. @@ -361,13 +366,8 @@ async def pack(self) -> StickerPack: :class:`StickerPack` The retrieved sticker pack. """ - data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs() - packs = data['sticker_packs'] - pack = find(lambda d: int(d['id']) == self.pack_id, packs) - - if pack: - return StickerPack(state=self._state, data=pack) - raise InvalidData(f'Could not find corresponding sticker pack for {self!r}') + data = await self._state.http.get_sticker_pack(self.pack_id) + return StickerPack(state=self._state, data=data) class GuildSticker(Sticker): diff --git a/discord/subscription.py b/discord/subscription.py new file mode 100644 index 000000000000..ec6d7c3e5ba5 --- /dev/null +++ b/discord/subscription.py @@ -0,0 +1,107 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import datetime +from typing import List, Optional, TYPE_CHECKING + +from . import utils +from .mixins import Hashable +from .enums import try_enum, SubscriptionStatus + +if TYPE_CHECKING: + from .state import ConnectionState + from .types.subscription import Subscription as SubscriptionPayload + from .user import User + +__all__ = ('Subscription',) + + +class Subscription(Hashable): + """Represents a Discord subscription. + + .. versionadded:: 2.5 + + Attributes + ----------- + id: :class:`int` + The subscription's ID. + user_id: :class:`int` + The ID of the user that is subscribed. + sku_ids: List[:class:`int`] + The IDs of the SKUs that the user subscribed to. + entitlement_ids: List[:class:`int`] + The IDs of the entitlements granted for this subscription. + current_period_start: :class:`datetime.datetime` + When the current billing period started. + current_period_end: :class:`datetime.datetime` + When the current billing period ends. + status: :class:`SubscriptionStatus` + The status of the subscription. + canceled_at: Optional[:class:`datetime.datetime`] + When the subscription was canceled. + This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`. + renewal_sku_ids: List[:class:`int`] + The IDs of the SKUs that the user is going to be subscribed to when renewing. + """ + + __slots__ = ( + '_state', + 'id', + 'user_id', + 'sku_ids', + 'entitlement_ids', + 'current_period_start', + 'current_period_end', + 'status', + 'canceled_at', + 'renewal_sku_ids', + ) + + def __init__(self, *, state: ConnectionState, data: SubscriptionPayload): + self._state = state + + self.id: int = int(data['id']) + self.user_id: int = int(data['user_id']) + self.sku_ids: List[int] = list(map(int, data['sku_ids'])) + self.entitlement_ids: List[int] = list(map(int, data['entitlement_ids'])) + self.current_period_start: datetime.datetime = utils.parse_time(data['current_period_start']) + self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end']) + self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status']) + self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at']) + self.renewal_sku_ids: List[int] = list(map(int, data['renewal_sku_ids'] or [])) + + def __repr__(self) -> str: + return f'' + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the subscription's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user that is subscribed.""" + return self._state.get_user(self.user_id) diff --git a/discord/threads.py b/discord/threads.py index b47c189d299f..024b22506b04 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -121,6 +121,10 @@ class Thread(Messageable, Hashable): This is always ``True`` for public threads. archiver_id: Optional[:class:`int`] The user's ID that archived this thread. + + .. note:: + Due to an API change, the ``archiver_id`` will always be ``None`` and can only be obtained via the audit log. + auto_archive_duration: :class:`int` The duration in minutes until the thread is automatically hidden from the channel list. Usually a value of 60, 1440, 4320 and 10080. @@ -188,7 +192,7 @@ def _from_data(self, data: ThreadPayload): self.me: Optional[ThreadMember] try: - member = data['member'] + member = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.me = None else: @@ -846,13 +850,21 @@ async def fetch_members(self) -> List[ThreadMember]: members = await self._state.http.get_thread_members(self.id) return [ThreadMember(parent=self, data=data) for data in members] - async def delete(self) -> None: + async def delete(self, *, reason: Optional[str] = None) -> None: """|coro| Deletes this thread. You must have :attr:`~Permissions.manage_threads` to delete threads. + Parameters + ----------- + reason: Optional[:class:`str`] + The reason for deleting this thread. + Shows up on the audit log. + + .. versionadded:: 2.4 + Raises ------- Forbidden @@ -860,7 +872,7 @@ async def delete(self) -> None: HTTPException Deleting the thread failed. """ - await self._state.http.delete_channel(self.id) + await self._state.http.delete_channel(self.id, reason=reason) def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index e291babfa3e0..9452bbbb150d 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -24,12 +24,13 @@ from __future__ import annotations -from typing import TypedDict, List, Optional +from typing import Literal, Dict, TypedDict, List, Optional from typing_extensions import NotRequired from .user import User from .team import Team from .snowflake import Snowflake +from .emoji import Emoji class InstallParams(TypedDict): @@ -37,6 +38,10 @@ class InstallParams(TypedDict): permissions: str +class AppIntegrationTypeConfig(TypedDict): + oauth2_install_params: NotRequired[InstallParams] + + class BaseAppInfo(TypedDict): id: Snowflake name: str @@ -45,6 +50,7 @@ class BaseAppInfo(TypedDict): summary: str description: str flags: int + approximate_user_install_count: NotRequired[int] cover_image: NotRequired[str] terms_of_service_url: NotRequired[str] privacy_policy_url: NotRequired[str] @@ -67,6 +73,7 @@ class AppInfo(BaseAppInfo): tags: NotRequired[List[str]] install_params: NotRequired[InstallParams] custom_install_url: NotRequired[str] + integration_types_config: NotRequired[Dict[Literal['0', '1'], AppIntegrationTypeConfig]] class PartialAppInfo(BaseAppInfo, total=False): @@ -78,3 +85,7 @@ class PartialAppInfo(BaseAppInfo, total=False): class GatewayAppInfo(TypedDict): id: Snowflake flags: int + + +class ListAppEmojis(TypedDict): + items: List[Emoji] diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index cd949709a479..2c37542fddc7 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -88,6 +88,9 @@ 111, 112, 121, + 130, + 131, + 132, 140, 141, 142, @@ -112,6 +115,7 @@ class _AuditLogChange_Str(TypedDict): 'permissions', 'tags', 'unicode_emoji', + 'emoji_name', ] new_value: str old_value: str @@ -136,6 +140,8 @@ class _AuditLogChange_Snowflake(TypedDict): 'channel_id', 'inviter_id', 'guild_id', + 'user_id', + 'sound_id', ] new_value: Snowflake old_value: Snowflake @@ -183,6 +189,12 @@ class _AuditLogChange_Int(TypedDict): old_value: int +class _AuditLogChange_Float(TypedDict): + key: Literal['volume'] + new_value: float + old_value: float + + class _AuditLogChange_ListRole(TypedDict): key: Literal['$add', '$remove'] new_value: List[Role] @@ -290,6 +302,7 @@ class _AuditLogChange_TriggerMetadata(TypedDict): _AuditLogChange_AssetHash, _AuditLogChange_Snowflake, _AuditLogChange_Int, + _AuditLogChange_Float, _AuditLogChange_Bool, _AuditLogChange_ListRole, _AuditLogChange_MFALevel, diff --git a/discord/types/channel.py b/discord/types/channel.py index d5d82b5c6461..4b593e55426a 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -28,6 +28,7 @@ from .user import PartialUser from .snowflake import Snowflake from .threads import ThreadMetadata, ThreadMember, ThreadArchiveDuration, ThreadType +from .emoji import PartialEmoji OverwriteType = Literal[0, 1] @@ -89,6 +90,20 @@ class VoiceChannel(_BaseTextChannel): video_quality_mode: NotRequired[VideoQualityMode] +VoiceChannelEffectAnimationType = Literal[0, 1] + + +class VoiceChannelEffect(TypedDict): + guild_id: Snowflake + channel_id: Snowflake + user_id: Snowflake + emoji: NotRequired[Optional[PartialEmoji]] + animation_type: NotRequired[VoiceChannelEffectAnimationType] + animation_id: NotRequired[int] + sound_id: NotRequired[Union[int, str]] + sound_volume: NotRequired[float] + + class CategoryChannel(_BaseGuildChannel): type: Literal[4] diff --git a/discord/types/command.py b/discord/types/command.py index f4eb41ef88eb..7876ee6ddf0e 100644 --- a/discord/types/command.py +++ b/discord/types/command.py @@ -29,9 +29,11 @@ from .channel import ChannelType from .snowflake import Snowflake +from .interactions import InteractionContextType ApplicationCommandType = Literal[1, 2, 3] ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +ApplicationIntegrationType = Literal[0, 1] class _BaseApplicationCommandOption(TypedDict): @@ -141,6 +143,8 @@ class _BaseApplicationCommand(TypedDict): id: Snowflake application_id: Snowflake name: str + contexts: List[InteractionContextType] + integration_types: List[ApplicationIntegrationType] dm_permission: NotRequired[Optional[bool]] default_member_permissions: NotRequired[Optional[str]] nsfw: NotRequired[bool] diff --git a/discord/types/components.py b/discord/types/components.py index 218f5cef07bf..bb241c9ac6ff 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -24,24 +24,31 @@ from __future__ import annotations -from typing import List, Literal, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired from .emoji import PartialEmoji from .channel import ChannelType -ComponentType = Literal[1, 2, 3, 4] -ButtonStyle = Literal[1, 2, 3, 4, 5] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] +ButtonStyle = Literal[1, 2, 3, 4, 5, 6] TextStyle = Literal[1, 2] -DefaultValueType = Literal['user', 'role', 'channel'] +DefaultValueType = Literal["user", "role", "channel"] +DividerSize = Literal[1, 2] +MediaItemLoadingState = Literal[0, 1, 2, 3] -class ActionRow(TypedDict): +class ComponentBase(TypedDict): + id: NotRequired[int] + type: int + + +class ActionRow(ComponentBase): type: Literal[1] components: List[ActionRowChildComponent] -class ButtonComponent(TypedDict): +class ButtonComponent(ComponentBase): type: Literal[2] style: ButtonStyle custom_id: NotRequired[str] @@ -49,6 +56,7 @@ class ButtonComponent(TypedDict): disabled: NotRequired[bool] emoji: NotRequired[PartialEmoji] label: NotRequired[str] + sku_id: NotRequired[str] class SelectOption(TypedDict): @@ -59,7 +67,7 @@ class SelectOption(TypedDict): emoji: NotRequired[PartialEmoji] -class SelectComponent(TypedDict): +class SelectComponent(ComponentBase): custom_id: str placeholder: NotRequired[str] min_values: NotRequired[int] @@ -98,7 +106,7 @@ class ChannelSelectComponent(SelectComponent): default_values: NotRequired[List[SelectDefaultValues]] -class TextInput(TypedDict): +class TextInput(ComponentBase): type: Literal[4] custom_id: str style: TextStyle @@ -117,5 +125,75 @@ class SelectMenu(SelectComponent): default_values: NotRequired[List[SelectDefaultValues]] +class SectionComponent(ComponentBase): + type: Literal[9] + components: List[Union[TextComponent, ButtonComponent]] + accessory: ComponentBase + + +class TextComponent(ComponentBase): + type: Literal[10] + content: str + + +class UnfurledMediaItem(TypedDict): + url: str + proxy_url: str + height: NotRequired[Optional[int]] + width: NotRequired[Optional[int]] + content_type: NotRequired[str] + placeholder: str + loading_state: MediaItemLoadingState + flags: NotRequired[int] + + +class ThumbnailComponent(ComponentBase): + type: Literal[11] + media: UnfurledMediaItem + description: NotRequired[Optional[str]] + spoiler: NotRequired[bool] + + +class MediaGalleryItem(TypedDict): + media: UnfurledMediaItem + description: NotRequired[Optional[str]] + spoiler: NotRequired[bool] + + +class MediaGalleryComponent(ComponentBase): + type: Literal[12] + items: List[MediaGalleryItem] + + +class FileComponent(ComponentBase): + type: Literal[13] + file: UnfurledMediaItem + spoiler: NotRequired[bool] + + +class SeparatorComponent(ComponentBase): + type: Literal[14] + divider: NotRequired[bool] + spacing: NotRequired[DividerSize] + + +class ContainerComponent(ComponentBase): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: List[ContainerChildComponent] + + ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] -Component = Union[ActionRow, ActionRowChildComponent] +ContainerChildComponent = Union[ + ActionRow, + TextComponent, + MediaGalleryComponent, + FileComponent, + SectionComponent, + SectionComponent, + ContainerComponent, + SeparatorComponent, + ThumbnailComponent, +] +Component = Union[ActionRowChildComponent, ContainerChildComponent] diff --git a/discord/types/embed.py b/discord/types/embed.py index f2f1c5a9f1e1..a18912f6e392 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -38,25 +38,12 @@ class EmbedField(TypedDict): inline: NotRequired[bool] -class EmbedThumbnail(TypedDict, total=False): - url: Required[str] - proxy_url: str - height: int - width: int - - -class EmbedVideo(TypedDict, total=False): - url: str - proxy_url: str - height: int - width: int - - -class EmbedImage(TypedDict, total=False): +class EmbedMedia(TypedDict, total=False): url: Required[str] proxy_url: str height: int width: int + flags: int class EmbedProvider(TypedDict, total=False): @@ -71,7 +58,7 @@ class EmbedAuthor(TypedDict, total=False): proxy_icon_url: str -EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link'] +EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link', 'poll_result'] class Embed(TypedDict, total=False): @@ -82,9 +69,10 @@ class Embed(TypedDict, total=False): timestamp: str color: int footer: EmbedFooter - image: EmbedImage - thumbnail: EmbedThumbnail - video: EmbedVideo + image: EmbedMedia + thumbnail: EmbedMedia + video: EmbedMedia provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] + flags: int diff --git a/discord/types/emoji.py b/discord/types/emoji.py index d54690c14417..85e7097576ca 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.py @@ -23,6 +23,7 @@ """ from typing import Optional, TypedDict +from typing_extensions import NotRequired from .snowflake import Snowflake, SnowflakeList from .user import User @@ -30,6 +31,7 @@ class PartialEmoji(TypedDict): id: Optional[Snowflake] name: Optional[str] + animated: NotRequired[bool] class Emoji(PartialEmoji, total=False): diff --git a/discord/types/gateway.py b/discord/types/gateway.py index fb450017e9c2..7dca5badc356 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -31,20 +31,22 @@ from .voice import GuildVoiceState from .integration import BaseIntegration, IntegrationApplication from .role import Role -from .channel import ChannelType, StageInstance +from .channel import ChannelType, StageInstance, VoiceChannelEffect from .interactions import Interaction from .invite import InviteTargetType from .emoji import Emoji, PartialEmoji from .member import MemberWithUser from .snowflake import Snowflake -from .message import Message +from .message import Message, ReactionType from .sticker import GuildSticker from .appinfo import GatewayAppInfo, PartialAppInfo from .guild import Guild, UnavailableGuild -from .user import User +from .user import User, AvatarDecorationData from .threads import Thread, ThreadMember from .scheduled_event import GuildScheduledEvent from .audit_log import AuditLogEntry +from .soundboard import SoundboardSound +from .subscription import Subscription class SessionStartLimit(TypedDict): @@ -90,8 +92,7 @@ class MessageDeleteBulkEvent(TypedDict): guild_id: NotRequired[Snowflake] -class MessageUpdateEvent(Message): - channel_id: Snowflake +MessageUpdateEvent = MessageCreateEvent class MessageReactionAddEvent(TypedDict): @@ -104,6 +105,7 @@ class MessageReactionAddEvent(TypedDict): message_author_id: NotRequired[Snowflake] burst: bool burst_colors: NotRequired[List[str]] + type: ReactionType class MessageReactionRemoveEvent(TypedDict): @@ -113,6 +115,7 @@ class MessageReactionRemoveEvent(TypedDict): emoji: PartialEmoji guild_id: NotRequired[Snowflake] burst: bool + type: ReactionType class MessageReactionRemoveAllEvent(TypedDict): @@ -228,6 +231,7 @@ class GuildMemberUpdateEvent(TypedDict): mute: NotRequired[bool] pending: NotRequired[bool] communication_disabled_until: NotRequired[str] + avatar_decoration_data: NotRequired[AvatarDecorationData] class GuildEmojisUpdateEvent(TypedDict): @@ -316,6 +320,19 @@ class _GuildScheduledEventUsersEvent(TypedDict): GuildScheduledEventUserAdd = GuildScheduledEventUserRemove = _GuildScheduledEventUsersEvent VoiceStateUpdateEvent = GuildVoiceState +VoiceChannelEffectSendEvent = VoiceChannelEffect + +GuildSoundBoardSoundCreateEvent = GuildSoundBoardSoundUpdateEvent = SoundboardSound + + +class GuildSoundBoardSoundsUpdateEvent(TypedDict): + guild_id: Snowflake + soundboard_sounds: List[SoundboardSound] + + +class GuildSoundBoardSoundDeleteEvent(TypedDict): + sound_id: Snowflake + guild_id: Snowflake class VoiceServerUpdateEvent(TypedDict): @@ -351,3 +368,14 @@ class GuildAuditLogEntryCreate(AuditLogEntry): EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement + + +class PollVoteActionEvent(TypedDict): + user_id: Snowflake + channel_id: Snowflake + message_id: Snowflake + guild_id: NotRequired[Snowflake] + answer_id: int + + +SubscriptionCreateEvent = SubscriptionUpdateEvent = SubscriptionDeleteEvent = Subscription diff --git a/discord/types/guild.py b/discord/types/guild.py index 44d51019a4fc..7ac90b89ea53 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -37,6 +37,7 @@ from .emoji import Emoji from .user import User from .threads import Thread +from .soundboard import SoundboardSound class Ban(TypedDict): @@ -49,6 +50,11 @@ class UnavailableGuild(TypedDict): unavailable: NotRequired[bool] +class IncidentData(TypedDict): + invites_disabled_until: NotRequired[Optional[str]] + dms_disabled_until: NotRequired[Optional[str]] + + DefaultMessageNotificationLevel = Literal[0, 1] ExplicitContentFilterLevel = Literal[0, 1, 2] MFALevel = Literal[0, 1] @@ -85,6 +91,8 @@ class UnavailableGuild(TypedDict): 'VIP_REGIONS', 'WELCOME_SCREEN_ENABLED', 'RAID_ALERTS_DISABLED', + 'SOUNDBOARD', + 'MORE_SOUNDBOARD', ] @@ -97,6 +105,7 @@ class _BaseGuildPreview(UnavailableGuild): stickers: List[GuildSticker] features: List[GuildFeature] description: Optional[str] + incidents_data: Optional[IncidentData] class _GuildPreviewUnique(TypedDict): @@ -148,6 +157,7 @@ class Guild(_BaseGuildPreview): max_members: NotRequired[int] premium_subscription_count: NotRequired[int] max_video_channel_users: NotRequired[int] + soundboard_sounds: NotRequired[List[SoundboardSound]] class InviteGuild(Guild, total=False): @@ -169,8 +179,8 @@ class GuildMFALevel(TypedDict): class ChannelPositionUpdate(TypedDict): id: Snowflake position: Optional[int] - lock_permissions: Optional[bool] - parent_id: Optional[Snowflake] + lock_permissions: NotRequired[Optional[bool]] + parent_id: NotRequired[Optional[Snowflake]] class _RolePositionRequired(TypedDict): @@ -179,3 +189,8 @@ class _RolePositionRequired(TypedDict): class RolePositionUpdate(_RolePositionRequired, total=False): position: Optional[Snowflake] + + +class BulkBanUserResponse(TypedDict): + banned_users: Optional[List[Snowflake]] + failed_users: Optional[List[Snowflake]] diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 52bb9c9972f8..3f3516c3a696 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -35,12 +35,25 @@ from .role import Role from .snowflake import Snowflake from .user import User +from .guild import GuildFeature if TYPE_CHECKING: from .message import Message InteractionType = Literal[1, 2, 3, 4, 5] +InteractionResponseType = Literal[ + 1, + 4, + 5, + 6, + 7, + 8, + 9, + 10, +] +InteractionContextType = Literal[0, 1, 2] +InteractionInstallationType = Literal[0, 1] class _BasePartialChannel(TypedDict): @@ -68,6 +81,12 @@ class ResolvedData(TypedDict, total=False): attachments: Dict[str, Attachment] +class PartialInteractionGuild(TypedDict): + id: Snowflake + locale: str + features: List[GuildFeature] + + class _BaseApplicationCommandInteractionDataOption(TypedDict): name: str @@ -204,6 +223,7 @@ class _BaseInteraction(TypedDict): token: str version: Literal[1] guild_id: NotRequired[Snowflake] + guild: NotRequired[PartialInteractionGuild] channel_id: NotRequired[Snowflake] channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel] app_permissions: NotRequired[str] @@ -211,6 +231,8 @@ class _BaseInteraction(TypedDict): guild_locale: NotRequired[str] entitlement_sku_ids: NotRequired[List[Snowflake]] entitlements: NotRequired[List[Entitlement]] + authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake] + context: NotRequired[InteractionContextType] class PingInteraction(_BaseInteraction): @@ -241,3 +263,75 @@ class MessageInteraction(TypedDict): name: str user: User member: NotRequired[Member] + + +class _MessageInteractionMetadata(TypedDict): + id: Snowflake + user: User + authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake] + original_response_message_id: NotRequired[Snowflake] + + +class _ApplicationCommandMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[2] + # command_type: Literal[1, 2, 3, 4] + + +class UserApplicationCommandMessageInteractionMetadata(_ApplicationCommandMessageInteractionMetadata): + # command_type: Literal[2] + target_user: User + + +class MessageApplicationCommandMessageInteractionMetadata(_ApplicationCommandMessageInteractionMetadata): + # command_type: Literal[3] + target_message_id: Snowflake + + +ApplicationCommandMessageInteractionMetadata = Union[ + _ApplicationCommandMessageInteractionMetadata, + UserApplicationCommandMessageInteractionMetadata, + MessageApplicationCommandMessageInteractionMetadata, +] + + +class MessageComponentMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[3] + interacted_message_id: Snowflake + + +class ModalSubmitMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[5] + triggering_interaction_metadata: Union[ + ApplicationCommandMessageInteractionMetadata, MessageComponentMessageInteractionMetadata + ] + + +MessageInteractionMetadata = Union[ + ApplicationCommandMessageInteractionMetadata, + MessageComponentMessageInteractionMetadata, + ModalSubmitMessageInteractionMetadata, +] + + +class InteractionCallbackResponse(TypedDict): + id: Snowflake + type: InteractionType + activity_instance_id: NotRequired[str] + response_message_id: NotRequired[Snowflake] + response_message_loading: NotRequired[bool] + response_message_ephemeral: NotRequired[bool] + + +class InteractionCallbackActivity(TypedDict): + id: str + + +class InteractionCallbackResource(TypedDict): + type: InteractionResponseType + activity_instance: NotRequired[InteractionCallbackActivity] + message: NotRequired[Message] + + +class InteractionCallback(TypedDict): + interaction: InteractionCallbackResponse + resource: NotRequired[InteractionCallbackResource] diff --git a/discord/types/invite.py b/discord/types/invite.py index b53ca374c6b2..f5f00078e950 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -35,6 +35,7 @@ from .appinfo import PartialAppInfo InviteTargetType = Literal[1, 2] +InviteType = Literal[0, 1, 2] class _InviteMetadata(TypedDict, total=False): @@ -63,6 +64,7 @@ class Invite(IncompleteInvite, total=False): target_type: InviteTargetType target_application: PartialAppInfo guild_scheduled_event: GuildScheduledEvent + type: InviteType class InviteWithCounts(Invite, _GuildPreviewUnique): diff --git a/discord/types/member.py b/discord/types/member.py index ad9e49008a12..88fb619fd398 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -24,7 +24,8 @@ from typing import Optional, TypedDict from .snowflake import SnowflakeList -from .user import User +from .user import User, AvatarDecorationData +from typing_extensions import NotRequired class Nickname(TypedDict): @@ -47,6 +48,8 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + banner: NotRequired[Optional[str]] + avatar_decoration_data: NotRequired[AvatarDecorationData] class _OptionalMemberWithUser(PartialMember, total=False): @@ -56,6 +59,7 @@ class _OptionalMemberWithUser(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + avatar_decoration_data: NotRequired[AvatarDecorationData] class MemberWithUser(_OptionalMemberWithUser): diff --git a/discord/types/message.py b/discord/types/message.py index 187db715a841..6c260d44dbdf 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -33,10 +33,11 @@ from .emoji import PartialEmoji from .embed import Embed from .channel import ChannelType -from .components import Component -from .interactions import MessageInteraction +from .components import ComponentBase +from .interactions import MessageInteraction, MessageInteractionMetadata from .sticker import StickerItem from .threads import Thread +from .poll import Poll class PartialMessage(TypedDict): @@ -56,6 +57,9 @@ class ReactionCountDetails(TypedDict): normal: int +ReactionType = Literal[0, 1] + + class Reaction(TypedDict): count: int me: bool @@ -98,7 +102,11 @@ class MessageApplication(TypedDict): cover_image: NotRequired[str] +MessageReferenceType = Literal[0, 1] + + class MessageReference(TypedDict, total=False): + type: MessageReferenceType message_id: Snowflake channel_id: Required[Snowflake] guild_id: Snowflake @@ -112,6 +120,24 @@ class RoleSubscriptionData(TypedDict): is_renewal: bool +PurchaseNotificationResponseType = Literal[0] + + +class GuildProductPurchase(TypedDict): + listing_id: Snowflake + product_name: str + + +class PurchaseNotificationResponse(TypedDict): + type: PurchaseNotificationResponseType + guild_product_purchase: Optional[GuildProductPurchase] + + +class CallMessage(TypedDict): + participants: SnowflakeList + ended_timestamp: NotRequired[Optional[str]] + + MessageType = Literal[ 0, 1, @@ -147,9 +173,25 @@ class RoleSubscriptionData(TypedDict): 37, 38, 39, + 44, + 46, ] +class MessageSnapshot(TypedDict): + type: MessageType + content: str + embeds: List[Embed] + attachments: List[Attachment] + timestamp: str + edited_timestamp: Optional[str] + flags: NotRequired[int] + mentions: List[UserWithMember] + mention_roles: SnowflakeList + sticker_items: NotRequired[List[StickerItem]] + components: NotRequired[List[ComponentBase]] + + class Message(PartialMessage): id: Snowflake author: User @@ -163,6 +205,7 @@ class Message(PartialMessage): attachments: List[Attachment] embeds: List[Embed] pinned: bool + poll: NotRequired[Poll] type: MessageType member: NotRequired[Member] mention_channels: NotRequired[List[ChannelMention]] @@ -176,11 +219,14 @@ class Message(PartialMessage): flags: NotRequired[int] sticker_items: NotRequired[List[StickerItem]] referenced_message: NotRequired[Optional[Message]] - interaction: NotRequired[MessageInteraction] - components: NotRequired[List[Component]] + interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata + interaction_metadata: NotRequired[MessageInteractionMetadata] + components: NotRequired[List[ComponentBase]] position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] thread: NotRequired[Thread] + call: NotRequired[CallMessage] + purchase_notification: NotRequired[PurchaseNotificationResponse] AllowedMentionType = Literal['roles', 'users', 'everyone'] diff --git a/discord/types/poll.py b/discord/types/poll.py new file mode 100644 index 000000000000..fabdbd48f7ad --- /dev/null +++ b/discord/types/poll.py @@ -0,0 +1,88 @@ +""" +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 List, TypedDict, Optional, Literal, TYPE_CHECKING +from typing_extensions import NotRequired + +from .snowflake import Snowflake + +if TYPE_CHECKING: + from .user import User + from .emoji import PartialEmoji + + +LayoutType = Literal[1] # 1 = Default + + +class PollMedia(TypedDict): + text: str + emoji: NotRequired[Optional[PartialEmoji]] + + +class PollAnswer(TypedDict): + poll_media: PollMedia + + +class PollAnswerWithID(PollAnswer): + answer_id: int + + +class PollAnswerCount(TypedDict): + id: Snowflake + count: int + me_voted: bool + + +class PollAnswerVoters(TypedDict): + users: List[User] + + +class PollResult(TypedDict): + is_finalized: bool + answer_counts: List[PollAnswerCount] + + +class PollCreate(TypedDict): + allow_multiselect: bool + answers: List[PollAnswer] + duration: float + layout_type: LayoutType + question: PollMedia + + +# We don't subclass Poll as it will +# still have the duration field, which +# is converted into expiry when poll is +# fetched from a message or returned +# by a `send` method in a Messageable +class Poll(TypedDict): + allow_multiselect: bool + answers: List[PollAnswerWithID] + expiry: str + layout_type: LayoutType + question: PollMedia + results: PollResult diff --git a/discord/types/sku.py b/discord/types/sku.py index 9ff3cfb13331..a49e0d6596f0 100644 --- a/discord/types/sku.py +++ b/discord/types/sku.py @@ -46,7 +46,8 @@ class Entitlement(TypedDict): deleted: bool starts_at: NotRequired[str] ends_at: NotRequired[str] - guild_id: Optional[str] + guild_id: NotRequired[str] + consumed: NotRequired[bool] EntitlementOwnerType = Literal[1, 2] diff --git a/discord/types/soundboard.py b/discord/types/soundboard.py new file mode 100644 index 000000000000..4910df8082f5 --- /dev/null +++ b/discord/types/soundboard.py @@ -0,0 +1,49 @@ +""" +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 typing import TypedDict, Optional, Union +from typing_extensions import NotRequired + +from .snowflake import Snowflake +from .user import User + + +class BaseSoundboardSound(TypedDict): + sound_id: Union[Snowflake, str] # basic string number when it's a default sound + volume: float + + +class SoundboardSound(BaseSoundboardSound): + name: str + emoji_name: Optional[str] + emoji_id: Optional[Snowflake] + user_id: NotRequired[Snowflake] + available: bool + guild_id: NotRequired[Snowflake] + user: NotRequired[User] + + +class SoundboardDefaultSound(BaseSoundboardSound): + name: str + emoji_name: str diff --git a/discord/types/subscription.py b/discord/types/subscription.py new file mode 100644 index 000000000000..8d4c020703c3 --- /dev/null +++ b/discord/types/subscription.py @@ -0,0 +1,43 @@ +""" +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 List, Literal, Optional, TypedDict + +from .snowflake import Snowflake + +SubscriptionStatus = Literal[0, 1, 2] + + +class Subscription(TypedDict): + id: Snowflake + user_id: Snowflake + sku_ids: List[Snowflake] + entitlement_ids: List[Snowflake] + current_period_start: str + current_period_end: str + status: SubscriptionStatus + canceled_at: Optional[str] + renewal_sku_ids: Optional[List[Snowflake]] diff --git a/discord/types/user.py b/discord/types/user.py index 7a34e44bb786..1f027ce9d9ac 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -24,6 +24,12 @@ from .snowflake import Snowflake from typing import Literal, Optional, TypedDict +from typing_extensions import NotRequired + + +class AvatarDecorationData(TypedDict): + asset: str + sku_id: Snowflake class PartialUser(TypedDict): @@ -32,6 +38,7 @@ class PartialUser(TypedDict): discriminator: str avatar: Optional[str] global_name: Optional[str] + avatar_decoration_data: NotRequired[AvatarDecorationData] PremiumType = Literal[0, 1, 2, 3] diff --git a/discord/types/voice.py b/discord/types/voice.py index 8f4e2e03e9e5..7e856ecddef0 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -29,7 +29,12 @@ from .member import MemberWithUser -SupportedModes = Literal['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'] +SupportedModes = Literal[ + 'aead_xchacha20_poly1305_rtpsize', + 'xsalsa20_poly1305_lite', + 'xsalsa20_poly1305_suffix', + 'xsalsa20_poly1305', +] class _VoiceState(TypedDict): diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index c5a51777ce3e..4d613f14faf0 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -16,3 +16,11 @@ from .select import * from .text_input import * from .dynamic import * +from .container import * +from .file import * +from .media_gallery import * +from .section import * +from .separator import * +from .text_display import * +from .thumbnail import * +from .action_row import * diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py new file mode 100644 index 000000000000..47903a4be1ec --- /dev/null +++ b/discord/ui/action_row.py @@ -0,0 +1,589 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +import sys +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Coroutine, + Dict, + Generator, + List, + Literal, + Optional, + Sequence, + Type, + TypeVar, + Union, + overload, +) + +from .item import Item, ItemCallbackType +from .button import Button, button as _button +from .dynamic import DynamicItem +from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect +from ..components import ActionRow as ActionRowComponent +from ..enums import ButtonStyle, ComponentType, ChannelType +from ..partial_emoji import PartialEmoji +from ..utils import MISSING, get as _utils_get + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + from .select import ( + BaseSelectT, + ValidDefaultValues, + MentionableSelectT, + ChannelSelectT, + RoleSelectT, + UserSelectT, + SelectT, + SelectCallbackDecorator, + ) + from ..emoji import Emoji + from ..components import SelectOption + from ..interactions import Interaction + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('ActionRow',) + + +class _ActionRowCallback: + __slots__ = ('row', 'callback', 'item') + + def __init__(self, callback: ItemCallbackType[Any], row: ActionRow, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any] = callback + self.row: ActionRow = row + self.item: Item[Any] = item + + def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: + return self.callback(self.row, interaction, self.item) + + +class ActionRow(Item[V]): + """Represents a UI action row. + + This is a top-level layout component that can only be used on :class:`LayoutView` + and can contain :class:`Button` 's and :class:`Select` 's in it. + + This can be inherited. + + .. note:: + + Action rows can contain up to 5 components, which is, 5 buttons or 1 select. + + .. versionadded:: 2.6 + + Examples + -------- + + .. code-block:: python3 + + import discord + from discord import ui + + # you can subclass it and add components with the decorators + class MyActionRow(ui.ActionRow): + @ui.button(label='Click Me!') + async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked me!') + + # or use it directly on LayoutView + class MyView(ui.LayoutView): + row = ui.ActionRow() + # or you can use your subclass: + # row = MyActionRow() + + # you can create items with row.button and row.select + @row.button(label='A button!') + async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked a button!') + + Parameters + ---------- + *children: :class:`Item` + The initial children of this action row. + row: Optional[:class:`int`] + The relative row this action row belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + __action_row_children_items__: ClassVar[List[ItemCallbackType[Any]]] = [] + __discord_ui_action_row__: ClassVar[bool] = True + __discord_ui_update_view__: ClassVar[bool] = True + + def __init__( + self, + *children: Item[V], + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + self._weight: int = 0 + self._children: List[Item[V]] = self._init_children() + self._children.extend(children) + self._weight += sum(i.width for i in children) + + if self._weight > 5: + raise ValueError('maximum number of children exceeded') + + self.id = id + self.row = row + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, ItemCallbackType[Any]] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if hasattr(member, '__discord_ui_model_type__'): + children[name] = member + + if len(children) > 5: + raise TypeError('ActionRow cannot have more than 5 children') + + cls.__action_row_children_items__ = list(children.values()) + + def _init_children(self) -> List[Item[Any]]: + children = [] + + for func in self.__action_row_children_items__: + item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item.callback = _ActionRowCallback(func, self, item) # type: ignore + item._parent = getattr(func, '__discord_ui_parent__', self) + setattr(self, func.__name__, item) + self._weight += item.width + children.append(item) + return children + + def _update_store_data(self, dispatch_info: Dict, dynamic_items: Dict) -> bool: + is_fully_dynamic = True + + for item in self._children: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item + is_fully_dynamic = False + return is_fully_dynamic + + def is_dispatchable(self) -> bool: + return any(c.is_dispatchable() for c in self.children) + + def is_persistent(self) -> bool: + return self.is_dispatchable() and all(c.is_persistent() for c in self.children) + + def _update_children_view(self, view: LayoutView) -> None: + for child in self._children: + child._view = view # pyright: ignore[reportAttributeAccessIssue] + + def _is_v2(self) -> bool: + # although it is not really a v2 component the only usecase here is for + # LayoutView which basically represents the top-level payload of components + # and ActionRow is only allowed there anyways. + # If the user tries to add any V2 component to a View instead of LayoutView + # it should error anyways. + return True + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.action_row]: + return ComponentType.action_row + + @property + def children(self) -> List[Item[V]]: + """List[:class:`Item`]: The list of children attached to this action row.""" + return self._children.copy() + + def walk_children(self) -> Generator[Item[V], Any, None]: + """An iterator that recursively walks through all the children of this view + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the action row. + """ + + for child in self.children: + yield child + + def add_item(self, item: Item[Any]) -> Self: + """Adds an item to this row. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to add to the row. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of children has been exceeded (5). + """ + + if len(self._children) >= 5: + raise ValueError('maximum number of children exceeded') + + if not isinstance(item, Item): + raise TypeError(f'expected Item not {item.__class__.__name__}') + + item._view = self._view + item._parent = self + self._children.append(item) + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children += 1 + + return self + + def remove_item(self, item: Item[Any]) -> Self: + """Removes an item from the row. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to remove from the view. + """ + + try: + self._children.remove(item) + except ValueError: + pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= 1 + + return self + + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`int` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + + def clear_items(self) -> Self: + """Removes all items from the row. + + This function returns the class instance to allow for fluent-style + chaining. + """ + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(self._children) + self._children.clear() + return self + + def to_component_dict(self) -> Dict[str, Any]: + components = [] + + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): + components.append(child.to_component_dict()) + + base = { + 'type': self.type.value, + 'components': components, + } + if self.id is not None: + base['id'] = self.id + return base + + def button( + self, + *, + label: Optional[str] = None, + custom_id: Optional[str] = None, + disabled: bool = False, + style: ButtonStyle = ButtonStyle.secondary, + emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + ) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]: + """A decorator that attaches a button to a component. + + The function being decorated should have three parameters, ``self`` representing + the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and + the :class:`discord.ui.Button` being pressed. + + .. note:: + + Buttons with a URL or a SKU cannot be created with this function. + Consider creating a :class:`Button` manually and adding it via + :meth:`ActionRow.add_item` instead. This is beacuse these buttons + cannot have a callback associated with them since Discord does not + do any processing with them. + + Parameters + ---------- + label: Optional[:class:`str`] + The label of the button, if any. + Can only be up to 80 characters. + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. + It is recommended to not set this parameters to prevent conflicts. + Can only be up to 100 characters. + style: :class:`.ButtonStyle` + The style of the button. Defaults to :attr:`.ButtonStyle.grey`. + disabled: :class:`bool` + Whether the button is disabled or not. Defaults to ``False``. + emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] + The emoji of the button. This can be in string form or a :class:`.PartialEmoji` + or a full :class:`.Emoji`. + """ + + def decorator(func: ItemCallbackType[Button[V]]) -> ItemCallbackType[Button[V]]: + ret = _button( + label=label, + custom_id=custom_id, + disabled=disabled, + style=style, + emoji=emoji, + row=None, + )(func) + ret.__discord_ui_parent__ = self # type: ignore + return ret # type: ignore + + return decorator # type: ignore + + @overload + def select( + self, + *, + cls: Type[SelectT] = Select[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + ) -> SelectCallbackDecorator[SelectT]: + ... + + @overload + def select( + self, + *, + cls: Type[UserSelectT] = UserSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[UserSelectT]: + ... + + @overload + def select( + self, + *, + cls: Type[RoleSelectT] = RoleSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[RoleSelectT]: + ... + + @overload + def select( + self, + *, + cls: Type[ChannelSelectT] = ChannelSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[ChannelSelectT]: + ... + + @overload + def select( + self, + *, + cls: Type[MentionableSelectT] = MentionableSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[MentionableSelectT]: + ... + + def select( + self, + *, + cls: Type[BaseSelectT] = Select[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = None, + custom_id: str = MISSING, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + default_values: Sequence[ValidDefaultValues] = MISSING, + ) -> SelectCallbackDecorator[BaseSelectT]: + """A decorator that attaches a select menu to a component. + + The function being decorated should have three parameters, ``self`` representing + the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and + the chosen select class. + + To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values + will depend on the type of select menu used. View the table below for more information. + + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | Select Type | Resolved Values | + +========================================+=================================================================================================================+ + | :class:`discord.ui.Select` | List[:class:`str`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + + Example + --------- + .. code-block:: python3 + + class ActionRow(discord.ui.ActionRow): + + @discord.ui.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text]) + async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect): + return await interaction.response.send_message(f'You selected {select.values[0].mention}') + + Parameters + ------------ + cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \ + Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]] + The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other + select types to display different select menus to the user. See the table above for the different + values you can get from each select type. Subclasses work as well, however the callback in the subclass will + get overridden. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`discord.SelectOption`] + A list of options that can be selected in this menu. This can only be used with + :class:`Select` instances. + Can only contain up to 25 items. + channel_types: List[:class:`~discord.ChannelType`] + The types of channels to show in the select menu. Defaults to all channels. This can only be used + with :class:`ChannelSelect` instances. + disabled: :class:`bool` + Whether the select is disabled or not. Defaults to ``False``. + default_values: Sequence[:class:`~discord.abc.Snowflake`] + A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. + If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. + Number of items must be in range of ``min_values`` and ``max_values``. + """ + + def decorator(func: ItemCallbackType[BaseSelectT]) -> ItemCallbackType[BaseSelectT]: + r = _select( # type: ignore + cls=cls, # type: ignore + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + options=options, + channel_types=channel_types, + disabled=disabled, + default_values=default_values, + )(func) + r.__discord_ui_parent__ = self + return r + + return decorator # type: ignore + + @classmethod + def from_component(cls, component: ActionRowComponent) -> ActionRow: + from .view import _component_to_item + + self = cls() + for cmp in component.children: + self.add_item(_component_to_item(cmp)) + return self diff --git a/discord/ui/button.py b/discord/ui/button.py index 2c051d12cb03..46230d480d54 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -42,11 +42,12 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import BaseView + from .action_row import ActionRow from ..emoji import Emoji from ..types.components import ButtonComponent as ButtonComponentPayload -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='BaseView', covariant=True) class Button(Item[V]): @@ -61,12 +62,14 @@ class Button(Item[V]): custom_id: Optional[:class:`str`] The ID of the button that gets received during an interaction. If this button is for a URL, it does not have a custom ID. + Can only be up to 100 characters. url: Optional[:class:`str`] The URL this button sends you to. disabled: :class:`bool` Whether the button is disabled or not. label: Optional[:class:`str`] The label of the button, if any. + Can only be up to 80 characters. emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] The emoji of the button, if available. row: Optional[:class:`int`] @@ -75,6 +78,15 @@ class Button(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + sku_id: Optional[:class:`int`] + The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` + nor ``custom_id``. + + .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + + .. versionadded:: 2.6 """ __item_repr_attributes__: Tuple[str, ...] = ( @@ -84,6 +96,7 @@ class Button(Item[V]): 'label', 'emoji', 'row', + 'sku_id', ) def __init__( @@ -96,13 +109,19 @@ def __init__( url: Optional[str] = None, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, + sku_id: Optional[int] = None, + id: Optional[int] = None, ): super().__init__() - if custom_id is not None and url is not None: - raise TypeError('cannot mix both url and custom_id with Button') + if custom_id is not None and (url is not None or sku_id is not None): + raise TypeError('cannot mix both url or sku_id and custom_id with Button') + + if url is not None and sku_id is not None: + raise TypeError('cannot mix both url and sku_id') + requires_custom_id = url is None and sku_id is None self._provided_custom_id = custom_id is not None - if url is None and custom_id is None: + if requires_custom_id and custom_id is None: custom_id = os.urandom(16).hex() if custom_id is not None and not isinstance(custom_id, str): @@ -111,6 +130,9 @@ def __init__( if url is not None: style = ButtonStyle.link + if sku_id is not None: + style = ButtonStyle.premium + if emoji is not None: if isinstance(emoji, str): emoji = PartialEmoji.from_str(emoji) @@ -126,8 +148,12 @@ def __init__( label=label, style=style, emoji=emoji, + sku_id=sku_id, + id=id, ) + self._parent: Optional[ActionRow] = None self.row = row + self.id = id @property def style(self) -> ButtonStyle: @@ -200,6 +226,20 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: else: self._underlying.emoji = None + @property + def sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: The SKU ID this button sends you to. + + .. versionadded:: 2.4 + """ + return self._underlying.sku_id + + @sku_id.setter + def sku_id(self, value: Optional[int]) -> None: + if value is not None: + self.style = ButtonStyle.premium + self._underlying.sku_id = value + @classmethod def from_component(cls, button: ButtonComponent) -> Self: return cls( @@ -210,6 +250,8 @@ def from_component(cls, button: ButtonComponent) -> Self: url=button.url, emoji=button.emoji, row=None, + sku_id=button.sku_id, + id=button.id, ) @property @@ -239,7 +281,8 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: + id: Optional[int] = None, +) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]: """A decorator that attaches a button to a component. The function being decorated should have three parameters, ``self`` representing @@ -248,19 +291,21 @@ def button( .. note:: - Buttons with a URL cannot be created with this function. + Buttons with a URL or an SKU cannot be created with this function. Consider creating a :class:`Button` manually instead. - This is because buttons with a URL do not have a callback + This is because these buttons cannot have a callback associated with them since Discord does not do any processing - with it. + with them. Parameters ------------ label: Optional[:class:`str`] The label of the button, if any. + Can only be up to 80 characters. custom_id: Optional[:class:`str`] The ID of the button that gets received during an interaction. It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. style: :class:`.ButtonStyle` The style of the button. Defaults to :attr:`.ButtonStyle.grey`. disabled: :class:`bool` @@ -274,9 +319,13 @@ def button( like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + + .. versionadded:: 2.6 """ - def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: + def decorator(func: ItemCallbackType[Button[V]]) -> ItemCallbackType[Button[V]]: if not inspect.iscoroutinefunction(func): raise TypeError('button function must be a coroutine function') @@ -289,6 +338,8 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto 'label': label, 'emoji': emoji, 'row': row, + 'sku_id': None, + 'id': id, } return func diff --git a/discord/ui/container.py b/discord/ui/container.py new file mode 100644 index 000000000000..72892d3a08eb --- /dev/null +++ b/discord/ui/container.py @@ -0,0 +1,432 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +import copy +import os +import sys +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Coroutine, + Dict, + Generator, + List, + Literal, + Optional, + Tuple, + Type, + TypeVar, + Union, +) + +from .item import Item, ItemCallbackType +from .view import _component_to_item, LayoutView +from .dynamic import DynamicItem +from ..enums import ComponentType +from ..utils import MISSING, get as _utils_get + +if TYPE_CHECKING: + from typing_extensions import Self + + from ..colour import Colour, Color + from ..components import Container as ContainerComponent + from ..interactions import Interaction + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('Container',) + + +class _ContainerCallback: + __slots__ = ('container', 'callback', 'item') + + def __init__(self, callback: ItemCallbackType[Any], container: Container, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any] = callback + self.container: Container = container + self.item: Item[Any] = item + + def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: + return self.callback(self.container, interaction, self.item) + + +class Container(Item[V]): + """Represents a UI container. + + This is a top-level layout component that can only be used on :class:`LayoutView` + and can contain :class:`ActionRow` 's, :class:`TextDisplay` 's, :class:`Section` 's, + :class:`MediaGallery` 's, and :class:`File` 's in it. + + This can be inherited. + + .. note:: + + Containers can contain up to 10 top-level components. + + .. versionadded:: 2.6 + + Examples + -------- + + .. code-block:: python3 + + import discord + from discord import ui + + # you can subclass it and add components as you would add them + # in a LayoutView + class MyContainer(ui.Container): + action_row = ui.ActionRow() + + @action_row.button(label='A button in a container!') + async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked a button!') + + # or use it directly on LayoutView + class MyView(ui.LayoutView): + container = ui.Container([ui.TextDisplay('I am a text display on a container!')]) + # or you can use your subclass: + # container = MyContainer() + + Parameters + ---------- + *children: :class:`Item` + The initial children of this container. + accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]] + The colour of the container. Defaults to ``None``. + accent_color: Optional[Union[:class:`.Colour`, :class:`int`]] + The color of the container. Defaults to ``None``. + spoiler: :class:`bool` + Whether to flag this container as a spoiler. Defaults + to ``False``. + row: Optional[:class:`int`] + The relative row this container belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + __container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Any], Item[Any]]]] = {} + __discord_ui_update_view__: ClassVar[bool] = True + __discord_ui_container__: ClassVar[bool] = True + + def __init__( + self, + *children: Item[V], + accent_colour: Optional[Union[Colour, int]] = None, + accent_color: Optional[Union[Color, int]] = None, + spoiler: bool = False, + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + self.__dispatchable: List[Item[V]] = [] + self._children: List[Item[V]] = self._init_children() + + if children is not MISSING: + for child in children: + self.add_item(child) + + self.spoiler: bool = spoiler + self._colour = accent_colour or accent_color + + self.row = row + self.id = id + + def _init_children(self) -> List[Item[Any]]: + children = [] + parents = {} + + for name, raw in self.__container_children_items__.items(): + if isinstance(raw, Item): + if getattr(raw, '__discord_ui_action_row__', False): + item = copy.deepcopy(raw) + # we need to deepcopy this object and set it later to prevent + # errors reported on the bikeshedding post + item._parent = self + + if item.is_dispatchable(): + self.__dispatchable.extend(item._children) # type: ignore + if getattr(raw, '__discord_ui_section__', False): + item = copy.copy(raw) + if item.accessory.is_dispatchable(): # type: ignore + item.accessory = copy.deepcopy(item.accessory) # type: ignore + if item.accessory._provided_custom_id is False: # type: ignore + item.accessory.custom_id = os.urandom(16).hex() # type: ignore + else: + item = copy.copy(raw) + + if getattr(item, '__discord_ui_section__', False) and item.accessory.is_dispatchable(): # type: ignore + self.__dispatchable.append(item.accessory) # type: ignore + + setattr(self, name, item) + children.append(item) + + parents[raw] = item + else: + # action rows can be created inside containers, and then callbacks can exist here + # so we create items based off them + item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) + item.callback = _ContainerCallback(raw, self, item) # type: ignore + setattr(self, raw.__name__, item) + # this should not fail because in order for a function to be here it should be from + # an action row and must have passed the check in __init_subclass__, but still + # guarding it + parent = getattr(raw, '__discord_ui_parent__', None) + if parent is None: + raise RuntimeError(f'{raw.__name__} is not a valid item for a Container') + parents.get(parent, parent)._children.append(item) + # we donnot append it to the children list because technically these buttons and + # selects are not from the container but the action row itself. + self.__dispatchable.append(item) + + return children + + def is_dispatchable(self) -> bool: + return bool(self.__dispatchable) + + def is_persistent(self) -> bool: + return self.is_dispatchable() and all(c.is_persistent() for c in self.children) + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, Union[ItemCallbackType[Any], Item[Any]]] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if isinstance(member, Item): + children[name] = member + if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): + children[name] = copy.copy(member) + + cls.__container_children_items__ = children + + def _update_children_view(self, view) -> None: + for child in self._children: + child._view = view + if getattr(child, '__discord_ui_update_view__', False): + # if the item is an action row which child's view can be updated, then update it + child._update_children_view(view) # type: ignore + + @property + def children(self) -> List[Item[V]]: + """List[:class:`Item`]: The children of this container.""" + return self._children.copy() + + @children.setter + def children(self, value: List[Item[V]]) -> None: + self._children = value + + @property + def accent_colour(self) -> Optional[Union[Colour, int]]: + """Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``.""" + return self._colour + + @accent_colour.setter + def accent_colour(self, value: Optional[Union[Colour, int]]) -> None: + self._colour = value + + accent_color = accent_colour + + @property + def type(self) -> Literal[ComponentType.container]: + return ComponentType.container + + @property + def width(self): + return 5 + + def _is_v2(self) -> bool: + return True + + def to_components(self) -> List[Dict[str, Any]]: + components = [] + + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): + components.append(child.to_component_dict()) + return components + + def to_component_dict(self) -> Dict[str, Any]: + components = self.to_components() + + colour = None + if self._colour: + colour = self._colour if isinstance(self._colour, int) else self._colour.value + + base = { + 'type': self.type.value, + 'accent_color': colour, + 'spoiler': self.spoiler, + 'components': components, + } + if self.id is not None: + base['id'] = self.id + return base + + def _update_store_data( + self, + dispatch_info: Dict[Tuple[int, str], Item[Any]], + dynamic_items: Dict[Any, Type[DynamicItem]], + ) -> bool: + is_fully_dynamic = True + for item in self.__dispatchable: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item + is_fully_dynamic = False + return is_fully_dynamic + + @classmethod + def from_component(cls, component: ContainerComponent) -> Self: + return cls( + *[_component_to_item(c) for c in component.children], + accent_colour=component.accent_colour, + spoiler=component.spoiler, + id=component.id, + ) + + def walk_children(self) -> Generator[Item[V], None, None]: + """An iterator that recursively walks through all the children of this container + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the container. + """ + + for child in self.children: + yield child + + if getattr(child, '__discord_ui_update_view__', False): + # if it has this attribute then it can contain children + yield from child.walk_children() # type: ignore + + def add_item(self, item: Item[Any]) -> Self: + """Adds an item to this container. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to append. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + """ + if not isinstance(item, Item): + raise TypeError(f'expected Item not {item.__class__.__name__}') + + self._children.append(item) + + if item.is_dispatchable(): + if getattr(item, '__discord_ui_section__', False): + self.__dispatchable.append(item.accessory) # type: ignore + elif hasattr(item, '_children'): + self.__dispatchable.extend([i for i in item._children if i.is_dispatchable()]) # type: ignore + else: + self.__dispatchable.append(item) + + is_layout_view = self._view and getattr(self._view, '__discord_ui_layout_view__', False) + + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self.view) # type: ignore + + if is_layout_view: + self._view.__total_children += len(tuple(item.walk_children())) # type: ignore + else: + if is_layout_view: + self._view.__total_children += 1 # type: ignore + + item._view = self.view + item._parent = self + return self + + def remove_item(self, item: Item[Any]) -> Self: + """Removes an item from this container. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`TextDisplay` + The item to remove from the section. + """ + + try: + self._children.remove(item) + except ValueError: + pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + if getattr(item, '__discord_ui_update_view__', False): + self._view.__total_children -= len(tuple(item.walk_children())) # type: ignore + else: + self._view.__total_children -= 1 + return self + + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`int` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + + def clear_items(self) -> Self: + """Removes all the items from the container. + + This function returns the class instance to allow for fluent-style + chaining. + """ + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(tuple(self.walk_children())) + self._children.clear() + return self diff --git a/discord/ui/dynamic.py b/discord/ui/dynamic.py index f3dcbf58a5ed..b8aa78fdbe30 100644 --- a/discord/ui/dynamic.py +++ b/discord/ui/dynamic.py @@ -38,14 +38,14 @@ from ..interactions import Interaction from ..components import Component from ..enums import ComponentType - from .view import View + from .view import BaseView - V = TypeVar('V', bound='View', covariant=True, default=View) + V = TypeVar('V', bound='BaseView', covariant=True, default=BaseView) else: - V = TypeVar('V', bound='View', covariant=True) + V = TypeVar('V', bound='BaseView', covariant=True) -class DynamicItem(Generic[BaseT], Item['View']): +class DynamicItem(Generic[BaseT], Item['BaseView']): """Represents an item with a dynamic ``custom_id`` that can be used to store state within that ``custom_id``. @@ -144,7 +144,7 @@ def is_persistent(self) -> bool: @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 + return self.item.custom_id @custom_id.setter def custom_id(self, value: str) -> None: @@ -154,7 +154,7 @@ def custom_id(self, value: str) -> None: 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.item.custom_id = value self._provided_custom_id = True @property @@ -208,3 +208,9 @@ async def from_custom_id( from the ``match`` object. """ raise NotImplementedError + + async def callback(self, interaction: Interaction[ClientT]) -> Any: + return await self.item.callback(interaction) + + async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool: + return await self.item.interaction_check(interaction) diff --git a/discord/ui/file.py b/discord/ui/file.py new file mode 100644 index 000000000000..5d9014e72acb --- /dev/null +++ b/discord/ui/file.py @@ -0,0 +1,145 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union + +from .item import Item +from ..components import FileComponent, UnfurledMediaItem +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('File',) + + +class File(Item[V]): + """Represents a UI file component. + + This is a top-level layout component that can only be used on :class:`LayoutView`. + + .. versionadded:: 2.6 + + Example + ------- + + .. code-block:: python3 + + import discord + from discord import ui + + class MyView(ui.LayoutView): + file = ui.File('attachment://file.txt') + # attachment://file.txt points to an attachment uploaded alongside this view + + Parameters + ---------- + media: Union[:class:`str`, :class:`.UnfurledMediaItem`] + This file's media. If this is a string it must point to a local + file uploaded within the parent view of this item, and must + meet the ``attachment://filename.extension`` structure. + spoiler: :class:`bool` + Whether to flag this file as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this file component belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + spoiler: bool = False, + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = FileComponent._raw_construct( + media=UnfurledMediaItem(media) if isinstance(media, str) else media, + spoiler=spoiler, + id=id, + ) + + self.row = row + self.id = id + + def _is_v2(self): + return True + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.file]: + return self._underlying.type + + @property + def media(self) -> UnfurledMediaItem: + """:class:`.UnfurledMediaItem`: Returns this file media.""" + return self._underlying.media + + @media.setter + def media(self, value: UnfurledMediaItem) -> None: + self._underlying.media = value + + @property + def url(self) -> str: + """:class:`str`: Returns this file's url.""" + return self._underlying.media.url + + @url.setter + def url(self, value: str) -> None: + self._underlying.media = UnfurledMediaItem(value) + + @property + def spoiler(self) -> bool: + """:class:`bool`: Returns whether this file should be flagged as a spoiler.""" + return self._underlying.spoiler + + @spoiler.setter + def spoiler(self, value: bool) -> None: + self._underlying.spoiler = value + + def to_component_dict(self): + return self._underlying.to_dict() + + @classmethod + def from_component(cls, component: FileComponent) -> Self: + return cls( + media=component.media, + spoiler=component.spoiler, + id=component.id, + ) diff --git a/discord/ui/item.py b/discord/ui/item.py index 1ee5492836b5..c73d8e7621ba 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -24,6 +24,7 @@ from __future__ import annotations +import os from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar from ..interactions import Interaction @@ -37,12 +38,12 @@ if TYPE_CHECKING: from ..enums import ComponentType - from .view import View + from .view import BaseView from ..components import Component I = TypeVar('I', bound='Item[Any]') -V = TypeVar('V', bound='View', covariant=True) -ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] +V = TypeVar('V', bound='BaseView', covariant=True) +ItemCallbackType = Callable[[Any, Interaction[Any], I], Coroutine[Any, Any, Any]] class Item(Generic[V]): @@ -53,6 +54,14 @@ class Item(Generic[V]): - :class:`discord.ui.Button` - :class:`discord.ui.Select` - :class:`discord.ui.TextInput` + - :class:`discord.ui.ActionRow` + - :class:`discord.ui.Container` + - :class:`discord.ui.File` + - :class:`discord.ui.MediaGallery` + - :class:`discord.ui.Section` + - :class:`discord.ui.Separator` + - :class:`discord.ui.TextDisplay` + - :class:`discord.ui.Thumbnail` .. versionadded:: 2.0 """ @@ -70,6 +79,14 @@ def __init__(self): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False + self._id: Optional[int] = None + self._max_row: int = 5 if not self._is_v2() else 40 + self._parent: Optional[Item] = None + + if self._is_v2(): + # this is done so v2 components can be stored on ViewStore._views + # and does not break v1 components custom_id property + self.custom_id: str = os.urandom(16).hex() def to_component_dict(self) -> Dict[str, Any]: raise NotImplementedError @@ -80,6 +97,9 @@ def _refresh_component(self, component: Component) -> None: def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None: return None + def _is_v2(self) -> bool: + return False + @classmethod def from_component(cls: Type[I], component: Component) -> I: return cls() @@ -92,7 +112,9 @@ def is_dispatchable(self) -> bool: return False def is_persistent(self) -> bool: - return self._provided_custom_id + if self.is_dispatchable(): + return self._provided_custom_id + return True def __repr__(self) -> str: attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__) @@ -106,10 +128,13 @@ def row(self) -> Optional[int]: def row(self, value: Optional[int]) -> None: if value is None: self._row = None - elif 5 > value >= 0: + elif self._max_row > value >= 0: self._row = value else: - raise ValueError('row cannot be negative or greater than or equal to 5') + raise ValueError(f'row cannot be negative or greater than or equal to {self._max_row}') + + if self._rendered_row is None: + self._rendered_row = value @property def width(self) -> int: @@ -120,6 +145,23 @@ def view(self) -> Optional[V]: """Optional[:class:`View`]: The underlying view for this item.""" return self._view + @property + def id(self) -> Optional[int]: + """Optional[:class:`int`]: The ID of this component.""" + return self._id + + @id.setter + def id(self, value: Optional[int]) -> None: + self._id = value + + async def _run_checks(self, interaction: Interaction[ClientT]) -> bool: + can_run = await self.interaction_check(interaction) + + if can_run and self._parent: + can_run = await self._parent._run_checks(interaction) + + return can_run + async def callback(self, interaction: Interaction[ClientT]) -> Any: """|coro| diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py new file mode 100644 index 000000000000..bbecc96494db --- /dev/null +++ b/discord/ui/media_gallery.py @@ -0,0 +1,189 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar + +from .item import Item +from ..enums import ComponentType +from ..components import ( + MediaGalleryItem, + MediaGalleryComponent, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('MediaGallery',) + + +class MediaGallery(Item[V]): + """Represents a UI media gallery. + + Can contain up to 10 :class:`.MediaGalleryItem` 's. + + This is a top-level layout component that can only be used on :class:`LayoutView`. + + .. versionadded:: 2.6 + + Parameters + ---------- + *items: :class:`.MediaGalleryItem` + The initial items of this gallery. + row: Optional[:class:`int`] + The relative row this media gallery belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + def __init__( + self, + *items: MediaGalleryItem, + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + + self._underlying = MediaGalleryComponent._raw_construct( + items=list(items), + id=id, + ) + + self.row = row + self.id = id + + @property + def items(self) -> List[MediaGalleryItem]: + """List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" + return self._underlying.items.copy() + + @items.setter + def items(self, value: List[MediaGalleryItem]) -> None: + if len(value) > 10: + raise ValueError('media gallery only accepts up to 10 items') + + self._underlying.items = value + + def to_component_dict(self): + return self._underlying.to_dict() + + def _is_v2(self) -> bool: + return True + + def add_item(self, item: MediaGalleryItem) -> Self: + """Adds an item to this gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`.MediaGalleryItem` + The item to add to the gallery. + + Raises + ------ + TypeError + A :class:`.MediaGalleryItem` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self._underlying.items) >= 10: + raise ValueError('maximum number of items has been exceeded') + + if not isinstance(item, MediaGalleryItem): + raise TypeError(f'expected MediaGalleryItem not {item.__class__.__name__}') + + self._underlying.items.append(item) + return self + + def remove_item(self, item: MediaGalleryItem) -> Self: + """Removes an item from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`.MediaGalleryItem` + The item to remove from the gallery. + """ + + try: + self._underlying.items.remove(item) + except ValueError: + pass + return self + + def insert_item_at(self, index: int, item: MediaGalleryItem) -> Self: + """Inserts an item before a specified index to the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + index: :class:`int` + The index of where to insert the item. + item: :class:`.MediaGalleryItem` + The item to insert. + """ + + self._underlying.items.insert(index, item) + return self + + def clear_items(self) -> Self: + """Removes all items from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + """ + + self._underlying.items.clear() + return self + + @property + def type(self) -> Literal[ComponentType.media_gallery]: + return self._underlying.type + + @property + def width(self): + return 5 + + @classmethod + def from_component(cls, component: MediaGalleryComponent) -> Self: + return cls( + *component.items, + id=component.id, + ) diff --git a/discord/ui/modal.py b/discord/ui/modal.py index b26fa9335b6c..630fc20f0c99 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -77,7 +77,8 @@ async def on_submit(self, interaction: discord.Interaction): Parameters ----------- title: :class:`str` - The title of the modal. Can only be up to 45 characters. + The title of the modal. + Can only be up to 45 characters. timeout: Optional[:class:`float`] Timeout in seconds from last interaction with the UI before no longer accepting input. If ``None`` then there is no timeout. diff --git a/discord/ui/section.py b/discord/ui/section.py new file mode 100644 index 000000000000..28688152fc1e --- /dev/null +++ b/discord/ui/section.py @@ -0,0 +1,253 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar + +from .item import Item +from .text_display import TextDisplay +from ..enums import ComponentType +from ..utils import MISSING, get as _utils_get + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + from ..components import SectionComponent + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('Section',) + + +class Section(Item[V]): + """Represents a UI section. + + This is a top-level layout component that can only be used on :class:`LayoutView` + + .. versionadded:: 2.6 + + Parameters + ---------- + *children: Union[:class:`str`, :class:`TextDisplay`] + The text displays of this section. Up to 3. + accessory: :class:`Item` + The section accessory. + row: Optional[:class:`int`] + The relative row this section belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + __discord_ui_section__: ClassVar[bool] = True + __discord_ui_update_view__: ClassVar[bool] = True + + __slots__ = ( + '_children', + 'accessory', + ) + + def __init__( + self, + *children: Union[Item[V], str], + accessory: Item[V], + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + self._children: List[Item[V]] = [] + if children is not MISSING: + if len(children) > 3: + raise ValueError('maximum number of children exceeded') + self._children.extend( + [c if isinstance(c, Item) else TextDisplay(c) for c in children], + ) + self.accessory: Item[V] = accessory + self.row = row + self.id = id + + @property + def type(self) -> Literal[ComponentType.section]: + return ComponentType.section + + @property + def children(self) -> List[Item[V]]: + """List[:class:`Item`]: The list of children attached to this section.""" + return self._children.copy() + + @property + def width(self): + return 5 + + def _is_v2(self) -> bool: + return True + + # Accessory can be a button, and thus it can have a callback so, maybe + # allow for section to be dispatchable and make the callback func + # be accessory component callback, only called if accessory is + # dispatchable? + def is_dispatchable(self) -> bool: + return self.accessory.is_dispatchable() + + def is_persistent(self) -> bool: + return self.is_dispatchable() and self.accessory.is_persistent() + + def walk_children(self) -> Generator[Item[V], None, None]: + """An iterator that recursively walks through all the children of this section. + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in this section. + """ + + for child in self.children: + yield child + yield self.accessory + + def _update_children_view(self, view) -> None: + self.accessory._view = view + + def add_item(self, item: Union[str, Item[Any]]) -> Self: + """Adds an item to this section. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: Union[:class:`str`, :class:`Item`] + The item to append, if it is a string it automatically wrapped around + :class:`TextDisplay`. + + Raises + ------ + TypeError + An :class:`Item` or :class:`str` was not passed. + ValueError + Maximum number of children has been exceeded (3). + """ + + if len(self._children) >= 3: + raise ValueError('maximum number of children exceeded') + + if not isinstance(item, (Item, str)): + raise TypeError(f'expected Item or str not {item.__class__.__name__}') + + item = item if isinstance(item, Item) else TextDisplay(item) + item._view = self.view + item._parent = self + self._children.append(item) + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children += 1 + + return self + + def remove_item(self, item: Item[Any]) -> Self: + """Removes an item from this section. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`TextDisplay` + The item to remove from the section. + """ + + try: + self._children.remove(item) + except ValueError: + pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= 1 + + return self + + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`int` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + + def clear_items(self) -> Self: + """Removes all the items from the section. + + This function returns the class instance to allow for fluent-style + chaining. + """ + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(self._children) + 1 # the + 1 is the accessory + + self._children.clear() + return self + + @classmethod + def from_component(cls, component: SectionComponent) -> Self: + from .view import _component_to_item # >circular import< + + return cls( + *[_component_to_item(c) for c in component.components], + accessory=_component_to_item(component.accessory), + id=component.id, + ) + + def to_component_dict(self) -> Dict[str, Any]: + data = { + 'type': self.type.value, + 'components': [ + c.to_component_dict() + for c in sorted( + self._children, + key=lambda i: i._rendered_row or sys.maxsize, + ) + ], + 'accessory': self.accessory.to_component_dict(), + } + if self.id is not None: + data['id'] = self.id + return data diff --git a/discord/ui/select.py b/discord/ui/select.py index 47c9d4a4779c..40b8a26f38dc 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import ( Any, @@ -69,9 +70,10 @@ ) if TYPE_CHECKING: - from typing_extensions import TypeAlias, Self, TypeGuard + from typing_extensions import TypeAlias, TypeGuard - from .view import View + from .view import BaseView + from .action_row import ActionRow from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import SelectMessageComponentInteractionData from ..app_commands import AppCommandChannel, AppCommandThread @@ -100,14 +102,14 @@ Thread, ] -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='BaseView', covariant=True) BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]') SelectT = TypeVar('SelectT', bound='Select[Any]') UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]') RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]') ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]') MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]') -SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] +SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[BaseSelectT]], BaseSelectT] DefaultSelectComponentTypes = Literal[ ComponentType.user_select, ComponentType.role_select, @@ -222,6 +224,7 @@ class BaseSelect(Item[V]): 'min_values', 'max_values', 'disabled', + 'id', ) def __init__( @@ -237,6 +240,7 @@ def __init__( options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, default_values: Sequence[SelectDefaultValue] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__() self._provided_custom_id = custom_id is not MISSING @@ -254,9 +258,12 @@ def __init__( channel_types=[] if channel_types is MISSING else channel_types, options=[] if options is MISSING else options, default_values=[] if default_values is MISSING else default_values, + id=id, ) self.row = row + self.id = id + self._parent: Optional[ActionRow] = None self._values: List[PossibleValue] = [] @property @@ -330,7 +337,9 @@ def _refresh_state(self, interaction: Interaction, data: SelectMessageComponentI values = selected_values.get({}) payload: List[PossibleValue] try: - resolved = Namespace._get_resolved_items(interaction, data['resolved']) + resolved = Namespace._get_resolved_items( + interaction, data['resolved'] # pyright: ignore[reportTypedDictNotRequiredAccess] + ) payload = list(resolved.values()) except KeyError: payload = data.get("values", []) # type: ignore @@ -342,7 +351,7 @@ def is_dispatchable(self) -> bool: return True @classmethod - def from_component(cls, component: SelectMenu) -> Self: + def from_component(cls, component: SelectMenu) -> BaseSelect[V]: type_to_cls: Dict[ComponentType, Type[BaseSelect[Any]]] = { ComponentType.string_select: Select, ComponentType.user_select: UserSelect, @@ -366,8 +375,10 @@ class Select(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -376,6 +387,7 @@ class Select(BaseSelect[V]): Defaults to 1 and must be between 1 and 25. options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. + Can only contain up to 25 items. disabled: :class:`bool` Whether the select is disabled or not. row: Optional[:class:`int`] @@ -384,6 +396,10 @@ class Select(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('options',) @@ -398,6 +414,7 @@ def __init__( options: List[SelectOption] = MISSING, disabled: bool = False, row: Optional[int] = None, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -408,6 +425,7 @@ def __init__( disabled=disabled, options=options, row=row, + id=id, ) @property @@ -455,7 +473,8 @@ def add_option( Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. - If not given, defaults to the label. Can only be up to 100 characters. + If not given, defaults to the label. + Can only be up to 100 characters. description: Optional[:class:`str`] An additional description of the option, if any. Can only be up to 100 characters. @@ -495,7 +514,7 @@ def append_option(self, option: SelectOption) -> None: The number of options exceeds 25. """ - if len(self._underlying.options) > 25: + if len(self._underlying.options) >= 25: raise ValueError('maximum number of options already provided') self._underlying.options.append(option) @@ -515,8 +534,10 @@ class UserSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -527,6 +548,7 @@ class UserSelect(BaseSelect[V]): 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. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -535,6 +557,10 @@ class UserSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -549,6 +575,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -559,6 +586,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -604,8 +632,10 @@ class RoleSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -616,6 +646,7 @@ class RoleSelect(BaseSelect[V]): Whether the select is disabled or not. default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the roles that should be selected by default. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -624,6 +655,10 @@ class RoleSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -638,6 +673,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -648,6 +684,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -688,8 +725,10 @@ class MentionableSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -701,6 +740,7 @@ class MentionableSelect(BaseSelect[V]): 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. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -709,6 +749,10 @@ class MentionableSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -723,6 +767,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -733,6 +778,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -778,10 +824,12 @@ class ChannelSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. channel_types: List[:class:`~discord.ChannelType`] The types of channels to show in the select menu. Defaults to all channels. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -792,6 +840,7 @@ class ChannelSelect(BaseSelect[V]): 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. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -800,6 +849,10 @@ class ChannelSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ( @@ -818,6 +871,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -829,6 +883,7 @@ def __init__( row=row, channel_types=channel_types, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -871,7 +926,7 @@ def default_values(self, value: Sequence[ValidDefaultValues]) -> None: @overload def select( *, - cls: Type[SelectT] = Select[V], + cls: Type[SelectT] = Select[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -880,14 +935,15 @@ def select( max_values: int = ..., disabled: bool = ..., row: Optional[int] = ..., -) -> SelectCallbackDecorator[V, SelectT]: + id: Optional[int] = ..., +) -> SelectCallbackDecorator[SelectT]: ... @overload def select( *, - cls: Type[UserSelectT] = UserSelect[V], + cls: Type[UserSelectT] = UserSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -897,14 +953,15 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., -) -> SelectCallbackDecorator[V, UserSelectT]: + id: Optional[int] = ..., +) -> SelectCallbackDecorator[UserSelectT]: ... @overload def select( *, - cls: Type[RoleSelectT] = RoleSelect[V], + cls: Type[RoleSelectT] = RoleSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -914,14 +971,15 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., -) -> SelectCallbackDecorator[V, RoleSelectT]: + id: Optional[int] = ..., +) -> SelectCallbackDecorator[RoleSelectT]: ... @overload def select( *, - cls: Type[ChannelSelectT] = ChannelSelect[V], + cls: Type[ChannelSelectT] = ChannelSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -931,14 +989,15 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., -) -> SelectCallbackDecorator[V, ChannelSelectT]: + id: Optional[int] = ..., +) -> SelectCallbackDecorator[ChannelSelectT]: ... @overload def select( *, - cls: Type[MentionableSelectT] = MentionableSelect[V], + cls: Type[MentionableSelectT] = MentionableSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, placeholder: Optional[str] = ..., @@ -948,13 +1007,14 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., -) -> SelectCallbackDecorator[V, MentionableSelectT]: + id: Optional[int] = ..., +) -> SelectCallbackDecorator[MentionableSelectT]: ... def select( *, - cls: Type[BaseSelectT] = Select[V], + cls: Type[BaseSelectT] = Select[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, placeholder: Optional[str] = None, @@ -964,7 +1024,8 @@ def select( disabled: bool = False, default_values: Sequence[ValidDefaultValues] = MISSING, row: Optional[int] = None, -) -> SelectCallbackDecorator[V, BaseSelectT]: + id: Optional[int] = None, +) -> SelectCallbackDecorator[BaseSelectT]: """A decorator that attaches a select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -1011,9 +1072,11 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe get overridden. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. custom_id: :class:`str` The ID of the select menu that gets received during an interaction. It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. row: Optional[:class:`int`] The relative row this select menu belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd @@ -1029,6 +1092,7 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. This can only be used with :class:`Select` instances. + Can only contain up to 25 items. channel_types: List[:class:`~discord.ChannelType`] The types of channels to show in the select menu. Defaults to all channels. This can only be used with :class:`ChannelSelect` instances. @@ -1037,11 +1101,16 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ - def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: + def decorator(func: ItemCallbackType[BaseSelectT]) -> ItemCallbackType[BaseSelectT]: if not inspect.iscoroutinefunction(func): raise TypeError('select function must be a coroutine function') callback_cls = getattr(cls, '__origin__', cls) @@ -1057,6 +1126,7 @@ def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, Bas 'min_values': min_values, 'max_values': max_values, 'disabled': disabled, + 'id': id, } if issubclass(callback_cls, Select): func.__discord_ui_model_kwargs__['options'] = options diff --git a/discord/ui/separator.py b/discord/ui/separator.py new file mode 100644 index 000000000000..e7d75a998b95 --- /dev/null +++ b/discord/ui/separator.py @@ -0,0 +1,127 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar + +from .item import Item +from ..components import SeparatorComponent +from ..enums import SeparatorSize, ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('Separator',) + + +class Separator(Item[V]): + """Represents a UI separator. + + This is a top-level layout component that can only be used on :class:`LayoutView`. + + .. versionadded:: 2.6 + + Parameters + ---------- + visible: :class:`bool` + Whether this separator is visible. On the client side this + is whether a divider line should be shown or not. + spacing: :class:`.SeparatorSize` + The spacing of this separator. + row: Optional[:class:`int`] + The relative row this separator belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + def __init__( + self, + *, + visible: bool = True, + spacing: SeparatorSize = SeparatorSize.small, + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = SeparatorComponent._raw_construct( + spacing=spacing, + visible=visible, + id=id, + ) + + self.row = row + self.id = id + + def _is_v2(self): + return True + + @property + def visible(self) -> bool: + """:class:`bool`: Whether this separator is visible. + + On the client side this is whether a divider line should + be shown or not. + """ + return self._underlying.visible + + @visible.setter + def visible(self, value: bool) -> None: + self._underlying.visible = value + + @property + def spacing(self) -> SeparatorSize: + """:class:`.SeparatorSize`: The spacing of this separator.""" + return self._underlying.spacing + + @spacing.setter + def spacing(self, value: SeparatorSize) -> None: + self._underlying.spacing = value + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.separator]: + return self._underlying.type + + def to_component_dict(self): + return self._underlying.to_dict() + + @classmethod + def from_component(cls, component: SeparatorComponent) -> Self: + return cls( + visible=component.visible, + spacing=component.spacing, + id=component.id, + ) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py new file mode 100644 index 000000000000..9ba7f294e4d0 --- /dev/null +++ b/discord/ui/text_display.py @@ -0,0 +1,96 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar + +from .item import Item +from ..components import TextDisplay as TextDisplayComponent +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('TextDisplay',) + + +class TextDisplay(Item[V]): + """Represents a UI text display. + + This is a top-level layout component that can only be used on :class:`LayoutView`. + + .. versionadded:: 2.6 + + Parameters + ---------- + content: :class:`str` + The content of this text display. Up to 4000 characters. + row: Optional[:class:`int`] + The relative row this text display belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[int] = None) -> None: + super().__init__() + self.content: str = content + + self.row = row + self.id = id + + def to_component_dict(self): + base = { + 'type': self.type.value, + 'content': self.content, + } + if self.id is not None: + base['id'] = self.id + return base + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.text_display]: + return ComponentType.text_display + + def _is_v2(self) -> bool: + return True + + @classmethod + def from_component(cls, component: TextDisplayComponent) -> Self: + return cls( + content=component.content, + id=component.id, + ) diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 23c1d874f285..86f7373ee11e 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -65,27 +65,37 @@ class TextInput(Item[V]): ------------ label: :class:`str` The label to display above the text input. + Can only be up to 45 characters. custom_id: :class:`str` The ID of the text input that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. style: :class:`discord.TextStyle` The style of the text input. placeholder: Optional[:class:`str`] The placeholder text to display when the text input is empty. + Can only be up to 100 characters. default: Optional[:class:`str`] The default value of the text input. + Can only be up to 4000 characters. required: :class:`bool` Whether the text input is required. min_length: Optional[:class:`int`] The minimum length of the text input. + Must be between 0 and 4000. max_length: Optional[:class:`int`] The maximum length of the text input. + Must be between 1 and 4000. row: Optional[:class:`int`] The relative row this text input 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). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __item_repr_attributes__: Tuple[str, ...] = ( @@ -106,6 +116,7 @@ def __init__( min_length: Optional[int] = None, max_length: Optional[int] = None, row: Optional[int] = None, + id: Optional[int] = None, ) -> None: super().__init__() self._value: Optional[str] = default @@ -123,8 +134,10 @@ def __init__( required=required, min_length=min_length, max_length=max_length, + id=id, ) self.row = row + self.id = id def __str__(self) -> str: return self.value @@ -235,6 +248,7 @@ def from_component(cls, component: TextInputComponent) -> Self: min_length=component.min_length, max_length=component.max_length, row=None, + id=component.id, ) @property diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py new file mode 100644 index 000000000000..67f8e4c7629f --- /dev/null +++ b/discord/ui/thumbnail.py @@ -0,0 +1,116 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union + +from .item import Item +from ..enums import ComponentType +from ..components import UnfurledMediaItem + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import LayoutView + from ..components import ThumbnailComponent + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('Thumbnail',) + + +class Thumbnail(Item[V]): + """Represents a UI Thumbnail. + + .. versionadded:: 2.6 + + Parameters + ---------- + media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] + The media of the thumbnail. This can be a URL or a reference + to an attachment that matches the ``attachment://filename.extension`` + structure. + description: Optional[:class:`str`] + The description of this thumbnail. Up to 256 characters. Defaults to ``None``. + spoiler: :class:`bool` + Whether to flag this thumbnail as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this thumbnail belongs to. By default + items are arranged automatically into those 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 39 (i.e. zero indexed) + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + """ + + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + description: Optional[str] = None, + spoiler: bool = False, + row: Optional[int] = None, + id: Optional[int] = None, + ) -> None: + super().__init__() + + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media + self.description: Optional[str] = description + self.spoiler: bool = spoiler + + self.row = row + self.id = id + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.thumbnail]: + return ComponentType.thumbnail + + def _is_v2(self) -> bool: + return True + + def to_component_dict(self) -> Dict[str, Any]: + base = { + 'type': self.type.value, + 'spoiler': self.spoiler, + 'media': self.media.to_dict(), + 'description': self.description, + } + if self.id is not None: + base['id'] = self.id + return base + + @classmethod + def from_component(cls, component: ThumbnailComponent) -> Self: + return cls( + media=component.media.url, + description=component.description, + spoiler=component.spoiler, + id=component.id, + ) diff --git a/discord/ui/view.py b/discord/ui/view.py index cff11d0905cc..7ecc9da1e482 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,23 @@ """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type + +from typing import ( + Any, + Callable, + ClassVar, + Coroutine, + Dict, + Generator, + Iterator, + List, + Optional, + Sequence, + TYPE_CHECKING, + Tuple, + Type, + Union, +) from functools import partial from itertools import groupby @@ -32,6 +48,7 @@ import sys import time import os +import copy from .item import Item, ItemCallbackType from .dynamic import DynamicItem from ..components import ( @@ -40,11 +57,19 @@ _component_factory, Button as ButtonComponent, SelectMenu as SelectComponent, + SectionComponent, + TextDisplay as TextDisplayComponent, + MediaGalleryComponent, + FileComponent, + SeparatorComponent, + ThumbnailComponent, ) +from ..utils import get as _utils_get # fmt: off __all__ = ( 'View', + 'LayoutView', ) # fmt: on @@ -55,11 +80,13 @@ from ..interactions import Interaction from ..message import Message - from ..types.components import Component as ComponentPayload + from ..types.components import ComponentBase as ComponentBasePayload from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload from ..state import ConnectionState from .modal import Modal + ItemLike = Union[ItemCallbackType[Any], Item[Any]] + _log = logging.getLogger(__name__) @@ -81,6 +108,30 @@ def _component_to_item(component: Component) -> Item: from .select import BaseSelect return BaseSelect.from_component(component) + if isinstance(component, SectionComponent): + from .section import Section + + return Section.from_component(component) + if isinstance(component, TextDisplayComponent): + from .text_display import TextDisplay + + return TextDisplay.from_component(component) + if isinstance(component, MediaGalleryComponent): + from .media_gallery import MediaGallery + + return MediaGallery.from_component(component) + if isinstance(component, FileComponent): + from .file import File + + return File.from_component(component) + if isinstance(component, SeparatorComponent): + from .separator import Separator + + return Separator.from_component(component) + if isinstance(component, ThumbnailComponent): + from .thumbnail import Thumbnail + + return Thumbnail.from_component(component) return Item.from_component(component) @@ -128,75 +179,74 @@ def remove_item(self, item: Item) -> None: def clear(self) -> None: self.weights = [0, 0, 0, 0, 0] + def v2_weights(self) -> bool: + return len(self.weights) > 5 + class _ViewCallback: __slots__ = ('view', 'callback', 'item') - def __init__(self, callback: ItemCallbackType[Any, Any], view: View, item: Item[View]) -> None: - self.callback: ItemCallbackType[Any, Any] = callback - self.view: View = view - self.item: Item[View] = item + def __init__(self, callback: ItemCallbackType[Any], view: BaseView, item: Item[BaseView]) -> None: + self.callback: ItemCallbackType[Any] = callback + self.view: BaseView = view + self.item: Item[BaseView] = item def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: return self.callback(self.view, interaction, self.item) -class View: - """Represents a UI view. - - This object must be inherited to create a UI within Discord. - - .. versionadded:: 2.0 - - Parameters - ----------- - timeout: Optional[:class:`float`] - Timeout in seconds from last interaction with the UI before no longer accepting input. - If ``None`` then there is no timeout. - """ - - __discord_ui_view__: ClassVar[bool] = True +class BaseView: + __discord_ui_view__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False - __view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - children: Dict[str, ItemCallbackType[Any, Any]] = {} - for base in reversed(cls.__mro__): - for name, member in base.__dict__.items(): - if hasattr(member, '__discord_ui_model_type__'): - children[name] = member - - if len(children) > 25: - raise TypeError('View cannot have more than 25 children') - - cls.__view_children_items__ = list(children.values()) - - def _init_children(self) -> List[Item[Self]]: - children = [] - for func in self.__view_children_items__: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) - item._view = self - setattr(self, func.__name__, item) - children.append(item) - return children + __discord_ui_container__: ClassVar[bool] = False + __view_children_items__: ClassVar[Dict[str, ItemLike]] = {} - def __init__(self, *, timeout: Optional[float] = 180.0): + def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self._children) self.id: str = os.urandom(16).hex() self._cache_key: Optional[int] = None - self.__cancel_callback: Optional[Callable[[View], None]] = None + self.__cancel_callback: Optional[Callable[[BaseView], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self.__total_children: int = len(tuple(self.walk_children())) + + def _is_v2(self) -> bool: + return False def __repr__(self) -> str: return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' + def _init_children(self) -> List[Item[Self]]: + children = [] + parents = {} + + for name, raw in self.__view_children_items__.items(): + if isinstance(raw, Item): + item = copy.deepcopy(raw) + setattr(self, name, item) + item._view = self + parent = getattr(item, '__discord_ui_parent__', None) + if parent and parent._view is None: + parent._view = self + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self) # type: ignore + children.append(item) + parents[raw] = item + else: + item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) + item.callback = _ViewCallback(raw, self, item) # type: ignore + item._view = self + setattr(self, raw.__name__, item) + parent = getattr(raw, '__discord_ui_parent__', None) + if parent: + parents.get(parent, parent)._children.append(item) + continue + children.append(item) + + return children + async def __timeout_task_impl(self) -> None: while True: # Guard just in case someone changes the value of the timeout at runtime @@ -214,25 +264,16 @@ async def __timeout_task_impl(self) -> None: # Wait N seconds to see if timeout data has been refreshed await asyncio.sleep(self.__timeout_expiry - now) - def to_components(self) -> List[Dict[str, Any]]: - def key(item: Item) -> int: - return item._rendered_row or 0 + def is_dispatchable(self) -> bool: + # this is used by webhooks to check whether a view requires a state attached + # or not, this simply is, whether a view has a component other than a url button + return any(item.is_dispatchable() for item in self.children) - children = sorted(self._children, key=key) - components: List[Dict[str, Any]] = [] - for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] - if not children: - continue - - components.append( - { - 'type': 1, - 'components': children, - } - ) + def has_components_v2(self) -> bool: + return any(c._is_v2() for c in self.children) - return components + def to_components(self) -> List[Dict[str, Any]]: + return NotImplemented def _refresh_timeout(self) -> None: if self.__timeout: @@ -263,7 +304,7 @@ def children(self) -> List[Item[Self]]: return self._children.copy() @classmethod - def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Any: """Converts a message's components into a :class:`View`. The :attr:`.Message.components` of a message are read-only @@ -277,28 +318,8 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) The message with components to convert into a view. timeout: Optional[:class:`float`] The timeout of the converted view. - - Returns - -------- - :class:`View` - The converted view. This always returns a :class:`View` and not - one of its subclasses. """ - view = View(timeout=timeout) - row = 0 - for component in message.components: - if isinstance(component, ActionRowComponent): - for child in component.children: - item = _component_to_item(child) - item.row = row - view.add_item(item) - row += 1 - else: - item = _component_to_item(component) - item.row = row - view.add_item(item) - - return view + pass def add_item(self, item: Item[Any]) -> Self: """Adds an item to the view. @@ -316,19 +337,26 @@ def add_item(self, item: Item[Any]) -> Self: TypeError An :class:`Item` was not passed. ValueError - Maximum number of children has been exceeded (25) - or the row the item is trying to be added to is full. + Maximum number of children has been exceeded, the + row the item is trying to be added to is full or the item + you tried to add is not allowed in this View. """ - if len(self._children) > 25: - raise ValueError('maximum number of children exceeded') - if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') - - self.__weights.add_item(item) + if item._is_v2() and not self._is_v2(): + raise ValueError('v2 items cannot be added to this view') item._view = self + added = 1 + + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self) # type: ignore + added += len(tuple(item.walk_children())) # type: ignore + + if self._is_v2() and self.__total_children + added > 40: + raise ValueError('maximum number of children exceeded') + self._children.append(item) return self @@ -349,7 +377,15 @@ def remove_item(self, item: Item[Any]) -> Self: except ValueError: pass else: - self.__weights.remove_item(item) + removed = 1 + if getattr(item, '__discord_ui_update_view__', False): + removed += len(tuple(item.walk_children())) # type: ignore + + if self.__total_children - removed < 0: + self.__total_children = 0 + else: + self.__total_children -= removed + return self def clear_items(self) -> Self: @@ -359,9 +395,31 @@ def clear_items(self) -> Self: chaining. """ self._children.clear() - self.__weights.clear() + self.__total_children = 0 return self + def get_item_by_id(self, id: int, /) -> Optional[Item[Self]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + .. versionadded:: 2.6 + + Parameters + ---------- + id: :class:`int` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + async def interaction_check(self, interaction: Interaction, /) -> bool: """|coro| @@ -420,7 +478,7 @@ async def _scheduled_task(self, item: Item, interaction: Interaction): try: item._refresh_state(interaction, interaction.data) # type: ignore - allow = await item.interaction_check(interaction) and await self.interaction_check(interaction) + allow = await item._run_checks(interaction) and await self.interaction_check(interaction) if not allow: return @@ -432,7 +490,7 @@ async def _scheduled_task(self, item: Item, interaction: Interaction): return await self.on_error(interaction, e, item) def _start_listening_from_store(self, store: ViewStore) -> None: - self.__cancel_callback = partial(store.remove_view) + self.__cancel_callback = partial(store.remove_view) # type: ignore if self.timeout: if self.__timeout_task is not None: self.__timeout_task.cancel() @@ -460,7 +518,7 @@ def _dispatch_item(self, item: Item, interaction: Interaction): def _refresh(self, components: List[Component]) -> None: # fmt: off old_state: Dict[str, Item[Any]] = { - item.custom_id: item # type: ignore + item.custom_id: item for item in self._children if item.is_dispatchable() } @@ -528,13 +586,268 @@ async def wait(self) -> bool: """ return await self.__stopped + def walk_children(self) -> Generator[Item[Any], None, None]: + """An iterator that recursively walks through all the children of this view + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the view. + """ + + for child in self.children: + yield child + + if getattr(child, '__discord_ui_update_view__', False): + # if it has this attribute then it can contain children + yield from child.walk_children() # type: ignore + + +class View(BaseView): + """Represents a UI view. + + This object must be inherited to create a UI within Discord. + + .. versionadded:: 2.0 + + Parameters + ----------- + timeout: Optional[:class:`float`] + Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. + """ + + __discord_ui_view__: ClassVar[bool] = True + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, ItemLike] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if hasattr(member, '__discord_ui_model_type__'): + children[name] = member + elif isinstance(member, Item) and member._is_v2(): + raise RuntimeError(f'{name} cannot be added to this View') + + if len(children) > 25: + raise TypeError('View cannot have more than 25 children') + + cls.__view_children_items__ = children + + def __init__(self, *, timeout: Optional[float] = 180.0): + super().__init__(timeout=timeout) + self.__weights = _ViewWeights(self._children) + + @property + def width(self): + return 5 + + def to_components(self) -> List[Dict[str, Any]]: + def key(item: Item) -> int: + return item._rendered_row or 0 + + children = sorted(self._children, key=key) + components: List[Dict[str, Any]] = [] + for _, group in groupby(children, key=key): + children = [item.to_component_dict() for item in group] + if not children: + continue + + components.append( + { + 'type': 1, + 'components': children, + } + ) + + return components + + @classmethod + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: + """Converts a message's components into a :class:`View`. + + The :attr:`.Message.components` of a message are read-only + and separate types from those in the ``discord.ui`` namespace. + In order to modify and edit message components they must be + converted into a :class:`View` first. + + .. warning:: + + This **will not** take into account every v2 component, if you + want to edit them, use :meth:`LayoutView.from_message` instead. + + Parameters + ----------- + message: :class:`discord.Message` + The message with components to convert into a view. + timeout: Optional[:class:`float`] + The timeout of the converted view. + + Returns + -------- + :class:`View` + The converted view. This always returns a :class:`View` and not + one of its subclasses. + """ + view = View(timeout=timeout) + row = 0 + for component in message.components: + if isinstance(component, ActionRowComponent): + for child in component.children: + item = _component_to_item(child) + item.row = row + if item._is_v2(): + raise RuntimeError('v2 components cannot be added to this View') + view.add_item(item) + row += 1 + else: + item = _component_to_item(component) + item.row = row + if item._is_v2(): + raise RuntimeError('v2 components cannot be added to this View') + view.add_item(item) + + return view + + def add_item(self, item: Item[Any]) -> Self: + if len(self._children) >= 25: + raise ValueError('maximum number of children exceeded') + + super().add_item(item) + try: + self.__weights.add_item(item) + except ValueError as e: + # if the item has no space left then remove it from _children + self._children.remove(item) + raise e + + return self + + def remove_item(self, item: Item[Any]) -> Self: + try: + self._children.remove(item) + except ValueError: + pass + else: + self.__weights.remove_item(item) + return self + + def clear_items(self) -> Self: + super().clear_items() + self.__weights.clear() + return self + + +class LayoutView(BaseView): + """Represents a layout view for components. + + This object must be inherited to create a UI within Discord. + + You can find usage examples in the :resource:`repository ` + + .. versionadded:: 2.6 + + Parameters + ---------- + timeout: Optional[:class:`float`] + Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. + """ + + __discord_ui_layout_view__: ClassVar[bool] = True + + def __init__(self, *, timeout: Optional[float] = 180.0) -> None: + super().__init__(timeout=timeout) + self.__total_children: int = len(list(self.walk_children())) + + if self.__total_children > 40: + raise ValueError('maximum number of children exceeded') + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, ItemLike] = {} + callback_children: Dict[str, ItemCallbackType[Any]] = {} + + row = 0 + + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if isinstance(member, Item): + if member._row is None: + member._row = row + member._rendered_row = member._row + children[name] = member + row += 1 + elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): + callback_children[name] = member + + children.update(callback_children) + cls.__view_children_items__ = children + + def _is_v2(self) -> bool: + return True + + def to_components(self): + components: List[Dict[str, Any]] = [] + + # sorted by row, which in LayoutView indicates the position of the component in the + # payload instead of in which ActionRow it should be placed on. + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): + components.append( + child.to_component_dict(), + ) + + return components + + def add_item(self, item: Item[Any]) -> Self: + if self.__total_children >= 40: + raise ValueError('maximum number of children exceeded (40)') + super().add_item(item) + return self + + @classmethod + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView: + """Converts a message's components into a :class:`LayoutView`. + + The :attr:`.Message.components` of a message are read-only + and separate types from those in the ``discord.ui`` namespace. + In order to modify and edit message components they must be + converted into a :class:`LayoutView` first. + + Unlike :meth:`View.from_message` this converts v2 components. + + Parameters + ----------- + message: :class:`discord.Message` + The message with components to convert into a view. + timeout: Optional[:class:`float`] + The timeout of the converted view. + + Returns + -------- + :class:`LayoutView` + The converted view. This always returns a :class:`LayoutView` and not + one of its subclasses. + """ + view = LayoutView(timeout=timeout) + for component in message.components: + item = _component_to_item(component) + item.row = 0 + view.add_item(item) + + return view + class ViewStore: def __init__(self, state: ConnectionState): # entity_id: {(component_type, custom_id): Item} - self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {} + self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {} # message_id: View - self._synced_message_views: Dict[int, View] = {} + self._synced_message_views: Dict[int, BaseView] = {} # custom_id: Modal self._modals: Dict[str, Modal] = {} # component_type is the key @@ -542,7 +855,7 @@ def __init__(self, state: ConnectionState): self._state: ConnectionState = state @property - def persistent_views(self) -> Sequence[View]: + def persistent_views(self) -> Sequence[BaseView]: # fmt: off views = { item.view.id: item.view @@ -563,7 +876,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: pattern = item.__discord_ui_compiled_template__ self._dynamic_items.pop(pattern, None) - def add_view(self, view: View, message_id: Optional[int] = None) -> None: + def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: view._start_listening_from_store(self) if view.__discord_ui_modal__: self._modals[view.custom_id] = view # type: ignore @@ -576,8 +889,34 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: pattern = item.__discord_ui_compiled_template__ self._dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore - is_fully_dynamic = False + if getattr(item, '__discord_ui_container__', False): + is_fully_dynamic = ( + item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) + or is_fully_dynamic + ) + elif getattr(item, '__discord_ui_action_row__', False): + is_fully_dynamic = ( + item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) + or is_fully_dynamic + ) + elif getattr(item, '__discord_ui_section__', False): + accessory: Item = item.accessory # type: ignore + accessory._view = view + + if isinstance(accessory, DynamicItem): + pattern = accessory.__discord_ui_compiled_template__ + self._dynamic_items[pattern] = accessory.__class__ + else: + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory + else: + dispatch_info[(item.type.value, item.custom_id)] = item + is_fully_dynamic = False view._cache_key = message_id if message_id is not None and not is_fully_dynamic: @@ -595,7 +934,7 @@ def remove_view(self, view: View) -> None: 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) if len(dispatch_info) == 0: self._views.pop(view._cache_key, None) @@ -615,16 +954,15 @@ async def schedule_dynamic_item_call( view = View.from_message(interaction.message, timeout=None) - 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: + try: + base_item_index, base_item = next( + (index, child) + for index, child in enumerate(view._children) + if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id + ) + except StopIteration: return - base_item = view._children[base_item_index] try: item = await factory.from_custom_id(interaction, base_item, match) except Exception: @@ -632,8 +970,9 @@ async def schedule_dynamic_item_call( return # Swap the item in the view with our new dynamic item - view._children[base_item_index] = item + view._children[base_item_index] = item # type: ignore item._view = view + item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore try: @@ -667,13 +1006,13 @@ def dispatch_view(self, component_type: int, custom_id: str, interaction: Intera msg = interaction.message if msg is not None: message_id = msg.id - if msg.interaction: - interaction_id = msg.interaction.id + if msg.interaction_metadata: + interaction_id = msg.interaction_metadata.id key = (component_type, custom_id) # The entity_id can either be message_id, interaction_id, or None in that priority order. - item: Optional[Item[View]] = None + item: Optional[Item[BaseView]] = None if message_id is not None: item = self._views.get(message_id, {}).get(key) @@ -725,14 +1064,14 @@ def remove_interaction_mapping(self, interaction_id: int) -> None: def is_message_tracked(self, message_id: int) -> bool: return message_id in self._synced_message_views - def remove_message_tracking(self, message_id: int) -> Optional[View]: + def remove_message_tracking(self, message_id: int) -> Optional[BaseView]: return self._synced_message_views.pop(message_id, None) - def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> None: + def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None: components: List[Component] = [] for component_data in data: - component = _component_factory(component_data) + component = _component_factory(component_data, self._state) # type: ignore if component is not None: components.append(component) diff --git a/discord/user.py b/discord/user.py index cc836374a40b..c5391372aa58 100644 --- a/discord/user.py +++ b/discord/user.py @@ -31,7 +31,7 @@ from .colour import Colour from .enums import DefaultAvatar from .flags import PublicUserFlags -from .utils import snowflake_time, _bytes_to_base64_data, MISSING +from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake if TYPE_CHECKING: from typing_extensions import Self @@ -43,10 +43,7 @@ from .message import Message from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload - from .types.user import ( - PartialUser as PartialUserPayload, - User as UserPayload, - ) + from .types.user import PartialUser as PartialUserPayload, User as UserPayload, AvatarDecorationData __all__ = ( @@ -73,6 +70,7 @@ class BaseUser(_UserTag): 'system', '_public_flags', '_state', + '_avatar_decoration_data', ) if TYPE_CHECKING: @@ -87,6 +85,7 @@ class BaseUser(_UserTag): _banner: Optional[str] _accent_colour: Optional[int] _public_flags: int + _avatar_decoration_data: Optional[AvatarDecorationData] def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: self._state = state @@ -123,6 +122,7 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None: self._public_flags = data.get('public_flags', 0) self.bot = data.get('bot', False) self.system = data.get('system', False) + self._avatar_decoration_data = data.get('avatar_decoration_data') @classmethod def _copy(cls, user: Self) -> Self: @@ -138,6 +138,7 @@ def _copy(cls, user: Self) -> Self: self.bot = user.bot self._state = user._state self._public_flags = user._public_flags + self._avatar_decoration_data = user._avatar_decoration_data return self @@ -170,7 +171,7 @@ def avatar(self) -> Optional[Asset]: @property def default_avatar(self) -> Asset: """:class:`Asset`: Returns the default avatar for a given user.""" - if self.discriminator == '0': + if self.discriminator in ('0', '0000'): avatar_id = (self.id >> 22) % len(DefaultAvatar) else: avatar_id = int(self.discriminator) % 5 @@ -187,6 +188,30 @@ def display_avatar(self) -> Asset: """ return self.avatar or self.default_avatar + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar decoration the user has. + + If the user has not set an avatar decoration, ``None`` is returned. + + .. versionadded:: 2.4 + """ + if self._avatar_decoration_data is not None: + return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data['asset']) + return None + + @property + def avatar_decoration_sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns the SKU ID of the avatar decoration the user has. + + If the user has not set an avatar decoration, ``None`` is returned. + + .. versionadded:: 2.4 + """ + if self._avatar_decoration_data is not None: + return _get_as_snowflake(self._avatar_decoration_data, 'sku_id') + return None + @property def banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the user's banner asset, if available. @@ -373,7 +398,9 @@ def _update(self, data: UserPayload) -> None: self._flags = data.get('flags', 0) self.mfa_enabled = data.get('mfa_enabled', False) - async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSING) -> ClientUser: + async def edit( + self, *, username: str = MISSING, avatar: Optional[bytes] = MISSING, banner: Optional[bytes] = MISSING + ) -> ClientUser: """|coro| Edits the current profile of the client. @@ -385,7 +412,6 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI then the file must be opened via ``open('some_filename', 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``. - The only image formats supported for uploading is JPEG and PNG. .. versionchanged:: 2.0 The edit is no longer in-place, instead the newly edited client user is returned. @@ -401,6 +427,13 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI avatar: Optional[:class:`bytes`] A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no banner. + Only image formats supported for uploading are JPEG, PNG, GIF and WEBP. + + .. versionadded:: 2.4 Raises ------ @@ -424,6 +457,12 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI else: payload['avatar'] = None + if banner is not MISSING: + if banner is not None: + payload['banner'] = _bytes_to_base64_data(banner) + else: + payload['banner'] = None + data: UserPayload = await self._state.http.edit_profile(payload) return ClientUser(state=self._state, data=data) diff --git a/discord/utils.py b/discord/utils.py index 33a4020a2504..bcdf922b402b 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import array @@ -41,7 +42,6 @@ Iterator, List, Literal, - Mapping, NamedTuple, Optional, Protocol, @@ -68,8 +68,10 @@ import os import sys import types +import typing import warnings import logging +import zlib import yarl @@ -80,6 +82,12 @@ else: HAS_ORJSON = True +try: + import zstandard # type: ignore +except ImportError: + _HAS_ZSTD = False +else: + _HAS_ZSTD = True __all__ = ( 'oauth_url', @@ -100,7 +108,7 @@ ) DISCORD_EPOCH = 1420070400000 -DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400 +DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760 class _MissingSentinel: @@ -147,8 +155,11 @@ def __get__(self, instance, owner): from .invite import Invite from .template import Template - class _RequestLike(Protocol): - headers: Mapping[str, Any] + class _DecompressionContext(Protocol): + COMPRESSION_TYPE: str + + def decompress(self, data: bytes, /) -> str | None: + ... P = ParamSpec('P') @@ -301,7 +312,7 @@ def decorated(*args: P.args, **kwargs: P.kwargs) -> T: else: fmt = '{0.__name__} is deprecated.' - warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning) + warnings.warn(fmt.format(func, instead), stacklevel=2, category=DeprecationWarning) warnings.simplefilter('default', DeprecationWarning) # reset filter return func(*args, **kwargs) @@ -316,7 +327,7 @@ def oauth_url( permissions: Permissions = MISSING, guild: Snowflake = MISSING, redirect_uri: str = MISSING, - scopes: Iterable[str] = MISSING, + scopes: Optional[Iterable[str]] = MISSING, disable_guild_select: bool = False, state: str = MISSING, ) -> str: @@ -358,7 +369,8 @@ def oauth_url( The OAuth2 URL for inviting the bot into guilds. """ url = f'https://discord.com/oauth2/authorize?client_id={client_id}' - url += '&scope=' + '+'.join(scopes or ('bot', 'applications.commands')) + if scopes is not None: + url += '&scope=' + '+'.join(scopes or ('bot', 'applications.commands')) if permissions is not MISSING: url += f'&permissions={permissions.value}' if guild is not MISSING: @@ -622,9 +634,19 @@ def _get_mime_type_for_image(data: bytes): raise ValueError('Unsupported image type given') -def _bytes_to_base64_data(data: bytes) -> str: +def _get_mime_type_for_audio(data: bytes): + if data.startswith(b'\x49\x44\x33') or data.startswith(b'\xff\xfb'): + return 'audio/mpeg' + else: + raise ValueError('Unsupported audio type given') + + +def _bytes_to_base64_data(data: bytes, *, audio: bool = False) -> str: fmt = 'data:{mime};base64,{data}' - mime = _get_mime_type_for_image(data) + if audio: + mime = _get_mime_type_for_audio(data) + else: + mime = _get_mime_type_for_image(data) b64 = b64encode(data).decode('ascii') return fmt.format(mime=mime, data=b64) @@ -692,13 +714,13 @@ async def maybe_coroutine(f: MaybeAwaitableFunc[P, T], *args: P.args, **kwargs: if _isawaitable(value): return await value else: - return value # type: ignore + return value async def async_all( gen: Iterable[Union[T, Awaitable[T]]], *, - check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, + check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, # type: ignore ) -> bool: for elem in gen: if check(elem): @@ -721,7 +743,7 @@ async def sane_wait_for(futures: Iterable[Awaitable[T]], *, timeout: Optional[fl def get_slots(cls: Type[Any]) -> Iterator[str]: for mro in reversed(cls.__mro__): try: - yield from mro.__slots__ # type: ignore + yield from mro.__slots__ except AttributeError: continue @@ -847,6 +869,12 @@ def resolve_invite(invite: Union[Invite, str]) -> ResolvedInvite: invite: Union[:class:`~discord.Invite`, :class:`str`] The invite. + Raises + ------- + ValueError + The invite is not a valid Discord invite, e.g. is not a URL + or does not contain alphanumeric characters. + Returns -------- :class:`.ResolvedInvite` @@ -866,7 +894,12 @@ def resolve_invite(invite: Union[Invite, str]) -> ResolvedInvite: event_id = url.query.get('event') return ResolvedInvite(code, int(event_id) if event_id else None) - return ResolvedInvite(invite, None) + + allowed_characters = r'[a-zA-Z0-9\-_]+' + if not re.fullmatch(allowed_characters, invite): + raise ValueError('Invite contains characters that are not allowed') + + return ResolvedInvite(invite, None) def resolve_template(code: Union[Template, str]) -> str: @@ -1080,6 +1113,7 @@ def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]: PY_310 = sys.version_info >= (3, 10) +PY_312 = sys.version_info >= (3, 12) def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: @@ -1087,7 +1121,7 @@ def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: literal_cls = type(Literal[0]) for p in parameters: if isinstance(p, literal_cls): - params.extend(p.__args__) + params.extend(p.__args__) # type: ignore else: params.append(p) return tuple(params) @@ -1118,6 +1152,16 @@ def evaluate_annotation( cache[tp] = evaluated return evaluated + if PY_312 and getattr(tp.__repr__, '__objclass__', None) is typing.TypeAliasType: # type: ignore + temp_locals = dict(**locals, **{t.__name__: t for t in tp.__type_params__}) + annotation = evaluate_annotation(tp.__value__, globals, temp_locals, cache.copy()) + if hasattr(tp, '__args__'): + annotation = annotation[tp.__args__] + return annotation + + if hasattr(tp, '__supertype__'): + return evaluate_annotation(tp.__supertype__, globals, locals, cache) + if hasattr(tp, '__metadata__'): # Annotated[X, Y] can access Y via __metadata__ metadata = tp.__metadata__[0] @@ -1394,3 +1438,105 @@ def _human_join(seq: Sequence[str], /, *, delimiter: str = ', ', final: str = 'o return f'{seq[0]} {final} {seq[1]}' return delimiter.join(seq[:-1]) + f' {final} {seq[-1]}' + + +if _HAS_ZSTD: + + class _ZstdDecompressionContext: + __slots__ = ('context',) + + COMPRESSION_TYPE: str = 'zstd-stream' + + def __init__(self) -> None: + decompressor = zstandard.ZstdDecompressor() + self.context = decompressor.decompressobj() + + def decompress(self, data: bytes, /) -> str | None: + # Each WS message is a complete gateway message + return self.context.decompress(data).decode('utf-8') + + _ActiveDecompressionContext: Type[_DecompressionContext] = _ZstdDecompressionContext +else: + + class _ZlibDecompressionContext: + __slots__ = ('context', 'buffer') + + COMPRESSION_TYPE: str = 'zlib-stream' + + def __init__(self) -> None: + self.buffer: bytearray = bytearray() + self.context = zlib.decompressobj() + + def decompress(self, data: bytes, /) -> str | None: + self.buffer.extend(data) + + # Check whether ending is Z_SYNC_FLUSH + if len(data) < 4 or data[-4:] != b'\x00\x00\xff\xff': + return + + msg = self.context.decompress(self.buffer) + self.buffer = bytearray() + + return msg.decode('utf-8') + + _ActiveDecompressionContext: Type[_DecompressionContext] = _ZlibDecompressionContext + + +def _format_call_duration(duration: datetime.timedelta) -> str: + seconds = duration.total_seconds() + + minutes_s = 60 + hours_s = minutes_s * 60 + days_s = hours_s * 24 + # Discord uses approx. 1/12 of 365.25 days (avg. days per year) + months_s = days_s * 30.4375 + years_s = months_s * 12 + + threshold_s = 45 + threshold_m = 45 + threshold_h = 21.5 + threshold_d = 25.5 + threshold_M = 10.5 + + if seconds < threshold_s: + formatted = "a few seconds" + elif seconds < (threshold_m * minutes_s): + minutes = round(seconds / minutes_s) + if minutes == 1: + formatted = "a minute" + else: + formatted = f"{minutes} minutes" + elif seconds < (threshold_h * hours_s): + hours = round(seconds / hours_s) + if hours == 1: + formatted = "an hour" + else: + formatted = f"{hours} hours" + elif seconds < (threshold_d * days_s): + days = round(seconds / days_s) + if days == 1: + formatted = "a day" + else: + formatted = f"{days} days" + elif seconds < (threshold_M * months_s): + months = round(seconds / months_s) + if months == 1: + formatted = "a month" + else: + formatted = f"{months} months" + else: + years = round(seconds / years_s) + if years == 1: + formatted = "a year" + else: + formatted = f"{years} years" + + return formatted + + +class _RawReprMixin: + __slots__: Tuple[str, ...] = () + + def __repr__(self) -> str: + value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) + return f'<{self.__class__.__name__} {value}>' diff --git a/discord/voice_client.py b/discord/voice_client.py index e5aa05e86df7..795434e1e722 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -125,7 +125,7 @@ async def on_voice_server_update(self, data: VoiceServerUpdatePayload, /) -> Non Parameters ------------ data: :class:`dict` - The raw :ddocs:`voice server update payload `. + The raw :ddocs:`voice server update payload `. """ raise NotImplementedError @@ -230,12 +230,13 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: self.timestamp: int = 0 self._player: Optional[AudioPlayer] = None self.encoder: Encoder = MISSING - self._lite_nonce: int = 0 + self._incr_nonce: int = 0 self._connection: VoiceConnectionState = self.create_connection_state() warn_nacl: bool = not has_nacl supported_modes: Tuple[SupportedModes, ...] = ( + 'aead_xchacha20_poly1305_rtpsize', 'xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305', @@ -337,7 +338,7 @@ async def disconnect(self, *, force: bool = False) -> None: Disconnects this voice client from voice. """ self.stop() - await self._connection.disconnect(force=force) + await self._connection.disconnect(force=force, wait=True) self.cleanup() async def move_to(self, channel: Optional[abc.Snowflake], *, timeout: Optional[float] = 30.0) -> None: @@ -380,7 +381,21 @@ def _get_voice_packet(self, data): encrypt_packet = getattr(self, '_encrypt_' + self.mode) return encrypt_packet(header, data) + def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: + # Esentially the same as _lite + # Uses an incrementing 32-bit integer which is appended to the payload + # The only other difference is we require AEAD with Additional Authenticated Data (the header) + box = nacl.secret.Aead(bytes(self.secret_key)) + nonce = bytearray(24) + + nonce[:4] = struct.pack('>I', self._incr_nonce) + self.checked_add('_incr_nonce', 1, 4294967295) + + return header + box.encrypt(bytes(data), bytes(header), bytes(nonce)).ciphertext + nonce[:4] + def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) nonce[:12] = header @@ -388,17 +403,21 @@ def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes: return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext def _encrypt_xsalsa20_poly1305_suffix(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) return header + box.encrypt(bytes(data), nonce).ciphertext + nonce def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) - nonce[:4] = struct.pack('>I', self._lite_nonce) - self.checked_add('_lite_nonce', 1, 4294967295) + nonce[:4] = struct.pack('>I', self._incr_nonce) + self.checked_add('_incr_nonce', 1, 4294967295) return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] @@ -567,6 +586,6 @@ def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None: try: self._connection.send_packet(packet) except OSError: - _log.info('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) + _log.debug('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295) diff --git a/discord/voice_state.py b/discord/voice_state.py index 6a680a106303..5f37a8de7fa8 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -1,639 +1,710 @@ -""" -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._disconnected = 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: - self._disconnected.set() - - # If we know we're going to get a voice_state_update where we have no channel due to - # being in the reconnect or disconnect 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: - await self._voice_disconnect() - if self.ws: - await self.ws.close() - 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() - - if self.socket: - self.socket.close() - - # Skip this part if disconnect was called from the poll loop task - if self._runner and asyncio.current_task() != self._runner: - # Wait for the voice_state_update event confirming the bot left the voice channel. - # This prevents a race condition caused by disconnecting and immediately connecting again. - # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still - # connecting leaving it in a bad state. Since there's no nice way to transfer state to the new one, we have to do this. - try: - async with atimeout(self.timeout): - await self._disconnected.wait() - except TimeoutError: - _log.debug('Timed out waiting for disconnect confirmation event') - - if cleanup: - self.voice_client.cleanup() - - 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 - self._disconnected.clear() - - 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): - # Don't call disconnect a second time if the websocket closed from a disconnect call - if not self._expecting_disconnect: - _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 - - previous_ws = self.ws - try: - self.ws = await self._connect_websocket(False) - await self._handshake_websocket() - except (ConnectionClosed, asyncio.TimeoutError): - return False - else: - return True - finally: - await previous_ws.close() - - 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 +""" +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 + +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, *, start_paused: bool = True) -> None: + super().__init__(daemon=True, name=f'voice-socket-reader:{id(self):#x}') + self.state: VoiceConnectionState = state + self.start_paused = start_paused + 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() + if self.start_paused: + self.pause() + 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, OSError) as e: + _log.debug( + "Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e + ) + # The socket is either closed or doesn't exist at the moment + continue + + 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._disconnected = 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: + self._disconnected.set() + + # If we know we're going to get a voice_state_update where we have no channel due to + # being in the reconnect or disconnect 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 + + channel_id = int(channel_id) + 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 + + # we moved ourselves + if channel_id != self.voice_client.channel.id: + self._update_voice_channel(channel_id) + + else: + self.state = ConnectionFlowState.got_both_voice_updates + return + + if self.state is ConnectionFlowState.connected: + self._update_voice_channel(channel_id) + + 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._update_voice_channel(channel_id) + 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: + previous_token = self.token + previous_server_id = self.server_id + previous_endpoint = self.endpoint + + 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 + 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: + # eventual consistency + if previous_token == self.token and previous_server_id == self.server_id and previous_endpoint == self.endpoint: + return + + _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 _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: + 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 + + async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: + _log.info('Connecting to voice...') + + await asyncio.wait_for( + self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), + timeout=timeout, + ) + _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, wait: bool = False) -> None: + if not force and not self.is_connected(): + return + + try: + await self._voice_disconnect() + if self.ws: + await self.ws.close() + except Exception: + _log.debug('Ignoring exception disconnecting from voice', exc_info=True) + finally: + self.state = ConnectionFlowState.disconnected + self._socket_reader.pause() + + # Stop threads before we unlock waiters so they end properly + if cleanup: + self._socket_reader.stop() + self.voice_client.stop() + + # Flip the connected event to unlock any waiters + self._connected.set() + self._connected.clear() + + if self.socket: + self.socket.close() + + self.ip = MISSING + self.port = MISSING + + # Skip this part if disconnect was called from the poll loop task + if wait and not self._inside_runner(): + # Wait for the voice_state_update event confirming the bot left the voice channel. + # This prevents a race condition caused by disconnecting and immediately connecting again. + # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still + # connecting leaving it in a bad state. Since there's no nice way to transfer state to the new one, we have to do this. + try: + await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) + except TimeoutError: + _log.debug('Timed out waiting for voice disconnection confirmation') + except asyncio.CancelledError: + pass + + if cleanup: + self.voice_client.cleanup() + + 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.state = with_state + self._socket_reader.pause() + + if self.socket: + self.socket.close() + + self.ip = MISSING + self.port = MISSING + + async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[float]) -> None: + if channel is None: + # This function should only be called externally so its ok to wait for the disconnect. + await self.disconnect(wait=True) + return + + if self.voice_client.channel and channel.id == self.voice_client.channel.id: + 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) + + def _inside_runner(self) -> bool: + return self._runner is not None and asyncio.current_task() == self._runner + + 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 + self._disconnected.clear() + + async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: + seq_ack = -1 + if self.ws is not MISSING: + seq_ack = self.ws.seq_ack + ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook, seq_ack=seq_ack) + 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, we should resume + if exc.code == 1000: + # Don't call disconnect a second time if the websocket closed from a disconnect call + if not self._expecting_disconnect: + _log.info('Disconnecting from voice normally, close code %d.', exc.code) + await self.disconnect() + break + + if exc.code == 4014: + # We were disconnected by discord + # This condition is a race between the main ws event and the voice ws closing + if self._disconnected.is_set(): + _log.info('Disconnected from voice by discord, close code %d.', exc.code) + await self.disconnect() + break + + # We may have been moved to a different channel + _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...') + # Don't bother to disconnect if already disconnected + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + else: + continue + + if exc.code == 4015: + _log.info('Disconnected from voice, attempting a resume...') + 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=True, + ) + except asyncio.TimeoutError: + _log.info('Could not resume the voice connection... Disconnecting...') + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + else: + _log.info('Successfully resumed voice connection') + 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, + ConnectionFlowState.disconnected, + timeout=self.timeout, + ) + except asyncio.TimeoutError: + return False + else: + if self.state is ConnectionFlowState.disconnected: + return False + + previous_ws = self.ws + try: + self.ws = await self._connect_websocket(False) + await self._handshake_websocket() + except (ConnectionClosed, asyncio.TimeoutError): + return False + else: + return True + finally: + await previous_ws.close() + + 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 + + def _update_voice_channel(self, channel_id: Optional[int]) -> None: + self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore \ No newline at end of file diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2a9a649e5514..104da78cab21 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -38,7 +38,7 @@ from .. import utils from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError from ..message import Message -from ..enums import try_enum, WebhookType, ChannelType +from ..enums import try_enum, WebhookType, ChannelType, DefaultAvatar from ..user import BaseUser, User from ..flags import MessageFlags from ..asset import Asset @@ -71,7 +71,8 @@ from ..emoji import Emoji from ..channel import VoiceChannel from ..abc import Snowflake - from ..ui.view import View + from ..ui.view import BaseView, View, LayoutView + from ..poll import Poll import datetime from ..types.webhook import ( Webhook as WebhookPayload, @@ -89,6 +90,9 @@ ) from ..types.emoji import PartialEmoji as PartialEmojiPayload from ..types.snowflake import SnowflakeList + from ..types.interactions import ( + InteractionCallback as InteractionCallbackResponsePayload, + ) BE = TypeVar('BE', bound=BaseException) _State = Union[ConnectionState, '_WebhookState'] @@ -309,8 +313,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> Response[Optional[MessagePayload]]: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -359,7 +364,7 @@ def edit_webhook_message( multipart: Optional[List[Dict[str, Any]]] = None, files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, - ) -> Response[Message]: + ) -> Response[MessagePayload]: route = Route( 'PATCH', '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', @@ -433,13 +438,14 @@ def create_interaction_response( proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, params: MultipartParameters, - ) -> Response[None]: + ) -> Response[InteractionCallbackResponsePayload]: route = Route( 'POST', '/interactions/{webhook_id}/{webhook_token}/callback', webhook_id=interaction_id, webhook_token=token, ) + request_params = {'with_response': '1'} if params.files: return self.request( @@ -449,9 +455,17 @@ def create_interaction_response( proxy_auth=proxy_auth, files=params.files, multipart=params.multipart, + params=request_params, ) else: - return self.request(route, session=session, proxy=proxy, proxy_auth=proxy_auth, payload=params.payload) + return self.request( + route, + session=session, + proxy=proxy, + proxy_auth=proxy_auth, + payload=params.payload, + params=request_params, + ) def get_original_interaction_response( self, @@ -538,9 +552,10 @@ def interaction_message_response_params( embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, ) -> MultipartParameters: if files is not MISSING and file is not MISSING: raise TypeError('Cannot mix file and files keyword arguments.') @@ -577,6 +592,13 @@ def interaction_message_response_params( if view is not MISSING: if view is not None: data['components'] = view.to_components() + + if view.has_components_v2(): + if flags is not MISSING: + flags.components_v2 = True + else: + flags = MessageFlags(components_v2=True) + else: data['components'] = [] @@ -608,6 +630,9 @@ def interaction_message_response_params( data['attachments'] = attachments_payload + if poll is not MISSING: + data['poll'] = poll._to_dict() + multipart = [] if files: data = {'type': type, 'data': data} @@ -655,6 +680,11 @@ def __init__(self, *, data: PartialChannelPayload) -> None: def __repr__(self) -> str: return f'' + @property + def mention(self) -> str: + """:class:`str`: The string that allows you to mention the channel that the webhook is following.""" + return f'<#{self.id}>' + class PartialWebhookGuild(Hashable): """Represents a partial guild for webhooks. @@ -779,7 +809,7 @@ async def edit( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, ) -> WebhookMessage: """|coro| @@ -1039,12 +1069,11 @@ def avatar(self) -> Optional[Asset]: @property def default_avatar(self) -> Asset: """ - :class:`Asset`: Returns the default avatar. This is always the blurple avatar. + :class:`Asset`: Returns the default avatar. .. versionadded:: 2.0 """ - # Default is always blurple apparently - return Asset._from_default_avatar(self._state, 0) + return Asset._from_default_avatar(self._state, (self.id >> 22) % len(DefaultAvatar)) @property def display_avatar(self) -> Asset: @@ -1155,7 +1184,7 @@ def __init__( self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth def __repr__(self) -> str: - return f'' + return f'' @property def url(self) -> str: @@ -1305,9 +1334,10 @@ def _as_follower(cls, data, *, channel, user) -> Self: 'user': { 'username': user.name, 'discriminator': user.discriminator, - 'global_name': user.global_name, 'id': user.id, 'avatar': user._avatar, + 'avatar_decoration_data': user._avatar_decoration_data, + 'global_name': user.global_name, }, } @@ -1575,6 +1605,46 @@ def _create_message(self, data, *, thread: Snowflake): # state is artificial return WebhookMessage(data=data, state=state, channel=channel) # type: ignore + @overload + async def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + ephemeral: bool = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[True], + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> WebhookMessage: + ... + + @overload + async def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + ephemeral: bool = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[False] = ..., + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> None: + ... + @overload async def send( self, @@ -1595,6 +1665,8 @@ async def send( wait: Literal[True], suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> WebhookMessage: ... @@ -1618,6 +1690,8 @@ async def send( wait: Literal[False] = ..., suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> None: ... @@ -1634,13 +1708,14 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: View = MISSING, + view: BaseView = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: bool = False, suppress_embeds: bool = False, silent: bool = False, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> Optional[WebhookMessage]: """|coro| @@ -1699,13 +1774,14 @@ async def send( Controls the mentions being processed in this message. .. versionadded:: 1.4 - view: :class:`discord.ui.View` - The view to send with the message. You can only send a view - if this webhook is not partial and has state attached. A - webhook has state attached if the webhook is managed by the - library. + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] + The view to send with the message. If the webhook is partial or + is not managed by the library, then you can only send URL buttons. + Otherwise, you can send views with any type of components. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. thread: :class:`~discord.abc.Snowflake` The thread to send this webhook to. @@ -1731,6 +1807,15 @@ async def send( .. versionadded:: 2.4 + poll: :class:`Poll` + The poll to send with this message. + + .. warning:: + + When sending a Poll via webhook, you cannot manually end it. + + .. versionadded:: 2.4 + Raises -------- HTTPException @@ -1746,7 +1831,8 @@ async def send( The length of ``embeds`` was invalid, there was no token associated with this webhook or ``ephemeral`` was passed with the improper webhook type or there was no state - attached with this webhook when giving it a view. + attached with this webhook when giving it a view that had + components other than URL buttons. Returns --------- @@ -1776,13 +1862,15 @@ async def send( wait = True if view is not MISSING: - if isinstance(self._state, _WebhookState): - raise ValueError('Webhook views require an associated state with the webhook') - if not hasattr(view, '__discord_ui_view__'): raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') - if ephemeral is True and view.timeout is None: + if isinstance(self._state, _WebhookState) and view.is_dispatchable(): + raise ValueError( + 'Webhook views with any component other than URL buttons require an associated state with the webhook' + ) + + if ephemeral is True and view.timeout is None and view.is_dispatchable(): view.timeout = 15 * 60.0 if thread_name is not MISSING and thread is not MISSING: @@ -1808,6 +1896,7 @@ async def send( allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, applied_tags=applied_tag_ids, + poll=poll, ) as params: adapter = async_context.get() thread_id: Optional[int] = None @@ -1825,6 +1914,7 @@ async def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) msg = None @@ -1835,6 +1925,9 @@ async def send( message_id = None if msg is None else msg.id self._state.store_view(view, message_id) + if poll is not MISSING and msg: + poll._update(msg) + return msg async def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> WebhookMessage: @@ -1887,6 +1980,33 @@ async def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> Web ) return self._create_message(data, thread=thread) + @overload + async def edit_message( + self, + message_id: int, + *, + attachments: Sequence[Union[Attachment, File]] = ..., + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> WebhookMessage: + ... + + @overload + async def edit_message( + self, + message_id: int, + *, + content: Optional[str] = ..., + embeds: Sequence[Embed] = ..., + embed: Optional[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + view: Optional[View] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> WebhookMessage: + ... + async def edit_message( self, message_id: int, @@ -1895,7 +2015,7 @@ async def edit_message( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, thread: Snowflake = MISSING, ) -> WebhookMessage: @@ -1934,12 +2054,14 @@ async def edit_message( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. The webhook must have state attached, similar to :meth:`send`. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. thread: :class:`~discord.abc.Snowflake` The thread the webhook message belongs to. diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 7da6ada70818..d5295c1fc0a6 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -44,7 +44,7 @@ from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError from ..message import Message, MessageFlags from ..http import Route, handle_message_parameters -from ..channel import PartialMessageable +from ..channel import PartialMessageable, ForumTag from .async_ import BaseWebhook, _WebhookState @@ -61,16 +61,19 @@ from ..file import File from ..embeds import Embed + from ..poll import Poll from ..mentions import AllowedMentions from ..message import Attachment from ..abc import Snowflake from ..state import ConnectionState + from ..ui.view import BaseView, View, LayoutView from ..types.webhook import ( Webhook as WebhookPayload, ) from ..types.message import ( Message as MessagePayload, ) + from ..types.snowflake import SnowflakeList BE = TypeVar('BE', bound=BaseException) @@ -288,8 +291,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> MessagePayload: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -608,7 +612,7 @@ def __init__( self.session: Session = session def __repr__(self) -> str: - return f'' + return f'' @property def url(self) -> str: @@ -852,6 +856,44 @@ def _create_message(self, data: MessagePayload, *, thread: Snowflake = MISSING) # state is artificial return SyncWebhookMessage(data=data, state=state, channel=channel) # type: ignore + @overload + def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[True], + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> SyncWebhookMessage: + ... + + @overload + def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[False] = ..., + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> None: + ... + @overload def send( self, @@ -870,6 +912,9 @@ def send( wait: Literal[True], suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, + view: View = MISSING, ) -> SyncWebhookMessage: ... @@ -891,6 +936,9 @@ def send( wait: Literal[False] = ..., suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, + view: View = MISSING, ) -> None: ... @@ -911,6 +959,9 @@ def send( wait: bool = False, suppress_embeds: bool = False, silent: bool = False, + applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, + view: BaseView = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. @@ -975,6 +1026,23 @@ def send( in the UI, but will not actually send a notification. .. versionadded:: 2.2 + poll: :class:`Poll` + The poll to send with this message. + + .. warning:: + + When sending a Poll via webhook, you cannot manually end it. + + .. versionadded:: 2.4 + view: Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`] + The view to send with the message. This can only have non-interactible items, which donnot + require a state to be attached to it. + + If you want to send a view with any component attached to it, check :meth:`Webhook.send`. + + .. versionadded:: 2.5 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. Raises -------- @@ -988,8 +1056,9 @@ def send( You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` or ``thread`` and ``thread_name``. ValueError - The length of ``embeds`` was invalid or - there was no token associated with this webhook. + The length of ``embeds`` was invalid, there was no token + associated with this webhook or you tried to send a view + with components other than URL buttons. Returns --------- @@ -1011,9 +1080,21 @@ def send( else: flags = MISSING + if view is not MISSING: + if not hasattr(view, '__discord_ui_view__'): + raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') + + if view.is_dispatchable(): + raise ValueError('SyncWebhook views can only contain URL buttons') + if thread_name is not MISSING and thread is not MISSING: raise TypeError('Cannot mix thread_name and thread keyword arguments.') + if applied_tags is MISSING: + applied_tag_ids = MISSING + else: + applied_tag_ids: SnowflakeList = [tag.id for tag in applied_tags] + with handle_message_parameters( content=content, username=username, @@ -1027,6 +1108,9 @@ def send( allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, flags=flags, + applied_tags=applied_tag_ids, + poll=poll, + view=view, ) as params: adapter: WebhookAdapter = _get_webhook_adapter() thread_id: Optional[int] = None @@ -1042,10 +1126,18 @@ def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) + msg = None + if wait: - return self._create_message(data, thread=thread) + msg = self._create_message(data, thread=thread) + + if poll is not MISSING and msg: + poll._update(msg) + + return msg def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> SyncWebhookMessage: """Retrieves a single :class:`~discord.SyncWebhookMessage` owned by this webhook. @@ -1093,6 +1185,33 @@ def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> SyncWebho ) return self._create_message(data, thread=thread) + @overload + def edit_message( + self, + message_id: int, + *, + attachments: Sequence[Union[Attachment, File]] = ..., + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> SyncWebhookMessage: + ... + + @overload + def edit_message( + self, + message_id: int, + *, + content: Optional[str] = ..., + embeds: Sequence[Embed] = ..., + embed: Optional[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + view: Optional[View] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> SyncWebhookMessage: + ... + def edit_message( self, message_id: int, @@ -1101,6 +1220,7 @@ def edit_message( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, thread: Snowflake = MISSING, ) -> SyncWebhookMessage: @@ -1127,6 +1247,13 @@ def edit_message( then all attachments are removed. .. versionadded:: 2.0 + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] + The updated view to update this message with. This can only have non-interactible items, which donnot + require a state to be attached to it. If ``None`` is passed then the view is removed. + + If you want to edit a webhook message with any component attached to it, check :meth:`WebhookMessage.edit`. + + .. versionadded:: 2.6 allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index d23392a548f5..1ca487c91a7d 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -214,4 +214,4 @@ async def edit( fields['enabled'] = enabled data = await self._state.http.edit_welcome_screen(self._guild.id, reason=reason, **fields) - return WelcomeScreen(data=data, guild=self._guild) + return self.__class__(data=data, guild=self._guild) diff --git a/discord/widget.py b/discord/widget.py index 8220086652d8..cdb883fd96db 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -184,7 +184,7 @@ def __init__( self.suppress: Optional[bool] = data.get('suppress', False) try: - game = data['game'] + game = data['game'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: activity = None else: diff --git a/docs/api.rst b/docs/api.rst index 095739721e0d..ebae882068ea 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -80,6 +80,14 @@ AppInstallParams .. autoclass:: AppInstallParams() :members: +IntegrationTypeConfig +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: IntegrationTypeConfig + +.. autoclass:: IntegrationTypeConfig() + :members: + Team ~~~~~ @@ -916,6 +924,29 @@ Members :param after: The updated member's updated info. :type after: :class:`Member` +.. function:: on_raw_presence_update(payload) + + Called when a :class:`Member` updates their presence. + + This requires :attr:`Intents.presences` to be enabled. + + Unlike :func:`on_presence_update`, when enabled, this is called regardless of the state of internal guild + and member caches, and **does not** provide a comparison between the previous and updated states of the :class:`Member`. + + .. important:: + + By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` + is disabled. + + You can manually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, + however :attr:`Intents.presences` is always required for this event to work. + + .. versionadded:: 2.5 + + :param payload: The raw presence update event model. + :type payload: :class:`RawPresenceUpdateEvent` + + Messages ~~~~~~~~~ @@ -1008,7 +1039,7 @@ Messages will return a :class:`Message` object that represents the message before the content was modified. Due to the inherently raw nature of this event, the data parameter coincides with - the raw data given by the :ddocs:`gateway `. + the raw data given by the :ddocs:`gateway `. Since the data payload can be partial, care must be taken when accessing stuff in the dictionary. One example of a common case of partial data is when the ``'content'`` key is inaccessible. This @@ -1047,6 +1078,47 @@ Messages :param payload: The raw event payload data. :type payload: :class:`RawBulkMessageDeleteEvent` +Polls +~~~~~~ + +.. function:: on_poll_vote_add(user, answer) + on_poll_vote_remove(user, answer) + + Called when a :class:`Poll` gains or loses a vote. If the ``user`` or ``answer``'s poll + parent message are not cached then this event will not be called. + + This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. + + .. note:: + + If the poll allows multiple answers and the user removes or adds multiple votes, this + event will be called as many times as votes that are added or removed. + + .. versionadded:: 2.4 + + :param user: The user that performed the action. + :type user: Union[:class:`User`, :class:`Member`] + :param answer: The answer the user voted or removed their vote from. + :type answer: :class:`PollAnswer` + +.. function:: on_raw_poll_vote_add(payload) + on_raw_poll_vote_remove(payload) + + Called when a :class:`Poll` gains or loses a vote. Unlike :func:`on_poll_vote_add` and :func:`on_poll_vote_remove` + this is called regardless of the state of the internal user and message cache. + + This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. + + .. note:: + + If the poll allows multiple answers and the user removes or adds multiple votes, this + event will be called as many times as votes that are added or removed. + + .. versionadded:: 2.4 + + :param payload: The raw event payload data. + :type payload: :class:`RawPollVoteActionEvent` + Reactions ~~~~~~~~~~ @@ -1257,6 +1329,35 @@ Scheduled Events :type user: :class:`User` +Soundboard +~~~~~~~~~~~ + +.. function:: on_soundboard_sound_create(sound) + on_soundboard_sound_delete(sound) + + Called when a :class:`SoundboardSound` is created or deleted. + + .. versionadded:: 2.5 + + :param sound: The soundboard sound that was created or deleted. + :type sound: :class:`SoundboardSound` + +.. function:: on_soundboard_sound_update(before, after) + + Called when a :class:`SoundboardSound` is updated. + + The following examples illustrate when this event is called: + + - The name is changed. + - The emoji is changed. + - The volume is changed. + + .. versionadded:: 2.5 + + :param sound: The soundboard sound that was updated. + :type sound: :class:`SoundboardSound` + + Stages ~~~~~~~ @@ -1286,6 +1387,37 @@ Stages :param after: The stage instance after the update. :type after: :class:`StageInstance` + +Subscriptions +~~~~~~~~~~~~~ + +.. function:: on_subscription_create(subscription) + + Called when a subscription is created. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was created. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_update(subscription) + + Called when a subscription is updated. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was updated. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_delete(subscription) + + Called when a subscription is deleted. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was deleted. + :type subscription: :class:`Subscription` + Threads ~~~~~~~~ @@ -1442,6 +1574,17 @@ Voice :param after: The voice state after the changes. :type after: :class:`VoiceState` +.. function:: on_voice_channel_effect(effect) + + Called when a :class:`Member` sends a :class:`VoiceChannelEffect` in a voice channel the bot is in. + + This requires :attr:`Intents.voice_states` to be enabled. + + .. versionadded:: 2.5 + + :param effect: The effect that is sent. + :type effect: :class:`VoiceChannelEffect` + .. _discord-api-utils: Utility Functions @@ -1769,6 +1912,16 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: purchase_notification + + The system message sent when a purchase is made in the guild. + + .. versionadded:: 2.5 + + .. attribute:: poll_result + + The system message sent when a poll has closed. + .. class:: UserFlags Represents Discord User flags. @@ -2265,7 +2418,7 @@ of :class:`enum.Enum`. When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with two attributes: - - ``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the members were moved. + - ``channel``: An :class:`abc.Connectable` or :class:`Object` with the channel ID where the members were moved. - ``count``: An integer specifying how many members were moved. .. versionadded:: 1.3 @@ -2904,6 +3057,42 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: soundboard_sound_create + + A soundboard sound was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + + .. attribute:: soundboard_sound_update + + A soundboard sound was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + + .. attribute:: soundboard_sound_delete + + A soundboard sound was deleted. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -3506,6 +3695,14 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: durable + + The SKU is a durable one-time purchase. + + .. attribute:: consumable + + The SKU is a consumable one-time purchase. + .. attribute:: subscription The SKU is a recurring subscription. @@ -3521,6 +3718,34 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: purchase + + The entitlement was purchased by the user. + + .. attribute:: premium_subscription + + The entitlement is for a nitro subscription. + + .. attribute:: developer_gift + + The entitlement was gifted by the developer. + + .. attribute:: test_mode_purchase + + The entitlement was purchased by a developer in application test mode. + + .. attribute:: free_purchase + + The entitlement was granted, when the SKU was free. + + .. attribute:: user_gift + + The entitlement was gifted by a another user. + + .. attribute:: premium_purchase + + The entitlement was claimed for free by a nitro subscriber. + .. attribute:: application_subscription The entitlement was purchased as an app subscription. @@ -3541,6 +3766,132 @@ of :class:`enum.Enum`. The entitlement owner is a user. +.. class:: PollLayoutType + + Represents how a poll answers are shown. + + .. versionadded:: 2.4 + + .. attribute:: default + + The default layout. + + +.. class:: InviteType + + Represents the type of an invite. + + .. versionadded:: 2.4 + + .. attribute:: guild + + The invite is a guild invite. + + .. attribute:: group_dm + + The invite is a group DM invite. + + .. attribute:: friend + + The invite is a friend invite. + + +.. class:: ReactionType + + Represents the type of a reaction. + + .. versionadded:: 2.4 + + .. attribute:: normal + + A normal reaction. + + .. attribute:: burst + + A burst reaction, also known as a "super reaction". + + +.. class:: VoiceChannelEffectAnimationType + + Represents the animation type of a voice channel effect. + + .. versionadded:: 2.5 + + .. attribute:: premium + + A fun animation, sent by a Nitro subscriber. + + .. attribute:: basic + + The standard animation. + + +.. class:: SubscriptionStatus + + Represents the status of an subscription. + + .. versionadded:: 2.5 + + .. attribute:: active + + The subscription is active. + + .. attribute:: ending + + The subscription is active but will not renew. + + .. attribute:: inactive + + The subscription is inactive and not being charged. + + +.. class:: MessageReferenceType + + Represents the type of a message reference. + + .. versionadded:: 2.5 + + .. attribute:: default + + A standard reference used by message replies (:attr:`MessageType.reply`), + crossposted messaged created by a followed channel integration, and messages of type: + + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` + + .. attribute:: forward + + A forwarded message. + + .. attribute:: reply + + An alias for :attr:`.default`. + + +.. class:: MediaItemLoadingState + + Represents a :class:`UnfurledMediaItem` load state. + + .. attribute:: unknown + + Unknown load state. + + .. attribute:: loading + + The media item is still loading. + + .. attribute:: loaded + + The media item is loaded. + + .. attribute:: not_found + + The media item was not found. + .. _discord-api-audit-logs: Audit Log Data @@ -4006,11 +4357,12 @@ AuditLogDiff .. attribute:: emoji - The name of the emoji that represents a sticker being changed. + The emoji which represents one of the following: - See also :attr:`GuildSticker.emoji`. + * :attr:`GuildSticker.emoji` + * :attr:`SoundboardSound.emoji` - :type: :class:`str` + :type: Union[:class:`str`, :class:`PartialEmoji`] .. attribute:: unicode_emoji @@ -4031,9 +4383,10 @@ AuditLogDiff .. attribute:: available - The availability of a sticker being changed. + The availability of one of the following being changed: - See also :attr:`GuildSticker.available` + * :attr:`GuildSticker.available` + * :attr:`SoundboardSound.available` :type: :class:`bool` @@ -4256,6 +4609,22 @@ AuditLogDiff :type: Optional[:class:`PartialEmoji`] + .. attribute:: user + + The user that represents the uploader of a soundboard sound. + + See also :attr:`SoundboardSound.user` + + :type: Union[:class:`Member`, :class:`User`] + + .. attribute:: volume + + The volume of a soundboard sound. + + See also :attr:`SoundboardSound.volume` + + :type: :class:`float` + .. this is currently missing the following keys: reason and application_id I'm not sure how to port these @@ -4491,6 +4860,32 @@ Guild :type: :class:`User` +.. class:: BulkBanResult + + A namedtuple which represents the result returned from :meth:`~Guild.bulk_ban`. + + .. versionadded:: 2.4 + + .. attribute:: banned + + The list of users that were banned. The inner :class:`Object` of the list + has the :attr:`Object.type` set to :class:`User`. + + :type: List[:class:`Object`] + .. attribute:: failed + + The list of users that could not be banned. The inner :class:`Object` of the list + has the :attr:`Object.type` set to :class:`User`. + + :type: List[:class:`Object`] + +GuildPreview +~~~~~~~~~~~~ + +.. attributetable:: GuildPreview + +.. autoclass:: GuildPreview + :members: ScheduledEvent ~~~~~~~~~~~~~~ @@ -4658,6 +5053,35 @@ VoiceChannel :members: :inherited-members: +.. attributetable:: VoiceChannelEffect + +.. autoclass:: VoiceChannelEffect() + :members: + :inherited-members: + +.. class:: VoiceChannelEffectAnimation + + A namedtuple which represents a voice channel effect animation. + + .. versionadded:: 2.5 + + .. attribute:: id + + The ID of the animation. + + :type: :class:`int` + .. attribute:: type + + The type of the animation. + + :type: :class:`VoiceChannelEffectAnimationType` + +.. attributetable:: VoiceChannelSoundEffect + +.. autoclass:: VoiceChannelSoundEffect() + :members: + :inherited-members: + StageChannel ~~~~~~~~~~~~~ @@ -4824,6 +5248,30 @@ GuildSticker .. autoclass:: GuildSticker() :members: +BaseSoundboardSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: BaseSoundboardSound + +.. autoclass:: BaseSoundboardSound() + :members: + +SoundboardDefaultSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SoundboardDefaultSound + +.. autoclass:: SoundboardDefaultSound() + :members: + +SoundboardSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SoundboardSound + +.. autoclass:: SoundboardSound() + :members: + ShardInfo ~~~~~~~~~~~ @@ -4832,6 +5280,14 @@ ShardInfo .. autoclass:: ShardInfo() :members: +SessionStartLimits +~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SessionStartLimits + +.. autoclass:: SessionStartLimits() + :members: + SKU ~~~~~~~~~~~ @@ -4848,6 +5304,14 @@ Entitlement .. autoclass:: Entitlement() :members: +Subscription +~~~~~~~~~~~~ + +.. attributetable:: Subscription + +.. autoclass:: Subscription() + :members: + RawMessageDeleteEvent ~~~~~~~~~~~~~~~~~~~~~~~ @@ -4952,6 +5416,22 @@ RawAppCommandPermissionsUpdateEvent .. autoclass:: RawAppCommandPermissionsUpdateEvent() :members: +RawPollVoteActionEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPollVoteActionEvent + +.. autoclass:: RawPollVoteActionEvent() + :members: + +RawPresenceUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPresenceUpdateEvent + +.. autoclass:: RawPresenceUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ @@ -4968,6 +5448,38 @@ PartialWebhookChannel .. autoclass:: PartialWebhookChannel() :members: +PollAnswer +~~~~~~~~~~ + +.. attributetable:: PollAnswer + +.. autoclass:: PollAnswer() + :members: + +MessageSnapshot +~~~~~~~~~~~~~~~~~ + +.. attributetable:: MessageSnapshot + +.. autoclass:: MessageSnapshot + :members: + +ClientStatus +~~~~~~~~~~~~ + +.. attributetable:: ClientStatus + +.. autoclass:: ClientStatus() + :members: + +CallMessage +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: CallMessage + +.. autoclass:: CallMessage() + :members: + .. _discord_api_data: Data Classes @@ -5041,6 +5553,22 @@ RoleSubscriptionInfo .. autoclass:: RoleSubscriptionInfo :members: +PurchaseNotification +~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PurchaseNotification + +.. autoclass:: PurchaseNotification() + :members: + +GuildProductPurchase ++++++++++++++++++++++ + +.. attributetable:: GuildProductPurchase + +.. autoclass:: GuildProductPurchase() + :members: + Intents ~~~~~~~~~~ @@ -5225,6 +5753,14 @@ SKUFlags .. autoclass:: SKUFlags() :members: +EmbedFlags +~~~~~~~~~~ + +.. attributetable:: EmbedFlags + +.. autoclass:: EmbedFlags() + :members: + ForumTag ~~~~~~~~~ @@ -5233,6 +5769,39 @@ ForumTag .. autoclass:: ForumTag :members: +Poll +~~~~ + +.. attributetable:: Poll + +.. autoclass:: Poll + :members: + +PollMedia +~~~~~~~~~ + +.. attributetable:: PollMedia + +.. autoclass:: PollMedia + :members: + +UnfurledMediaItem +~~~~~~~~~~~~~~~~~ + +.. attributetable:: UnfurledMediaItem + +.. autoclass:: UnfurledMediaItem + :members: + + +MediaGalleryItem +~~~~~~~~~~~~~~~~ + +.. attributetable:: MediaGalleryItem + +.. autoclass:: MediaGalleryItem + :members: + Exceptions ------------ @@ -5267,6 +5836,8 @@ The following exceptions are thrown by the library. .. autoexception:: InteractionResponded +.. autoexception:: MissingApplicationID + .. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusNotLoaded @@ -5284,6 +5855,7 @@ Exception Hierarchy - :exc:`ConnectionClosed` - :exc:`PrivilegedIntentsRequired` - :exc:`InteractionResponded` + - :exc:`MissingApplicationID` - :exc:`GatewayNotFound` - :exc:`HTTPException` - :exc:`Forbidden` diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index 9bda24f6e890..3da5cae163f8 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -531,6 +531,11 @@ Converters .. autoclass:: discord.ext.commands.ScheduledEventConverter :members: +.. attributetable:: discord.ext.commands.SoundboardSoundConverter + +.. autoclass:: discord.ext.commands.SoundboardSoundConverter + :members: + .. attributetable:: discord.ext.commands.clean_content .. autoclass:: discord.ext.commands.clean_content @@ -708,6 +713,9 @@ Exceptions .. autoexception:: discord.ext.commands.ScheduledEventNotFound :members: +.. autoexception:: discord.ext.commands.SoundboardSoundNotFound + :members: + .. autoexception:: discord.ext.commands.BadBoolArgument :members: @@ -800,6 +808,7 @@ Exception Hierarchy - :exc:`~.commands.EmojiNotFound` - :exc:`~.commands.GuildStickerNotFound` - :exc:`~.commands.ScheduledEventNotFound` + - :exc:`~.commands.SoundboardSoundNotFound` - :exc:`~.commands.PartialEmojiConversionFailure` - :exc:`~.commands.BadBoolArgument` - :exc:`~.commands.RangeError` diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 02a9ae670baf..52e57ff4dbe9 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -778,6 +778,19 @@ This tells the parser that the ``members`` attribute is mapped to a flag named ` the default value is an empty list. For greater customisability, the default can either be a value or a callable that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine. +A positional flag can be defined by setting the :attr:`~ext.commands.Flag.positional` attribute to ``True``. This +tells the parser that the content provided before the parsing occurs is part of the flag. This is useful for commands that +require a parameter to be used first and the flags are optional, such as the following: + +.. code-block:: python3 + + class BanFlags(commands.FlagConverter): + members: List[discord.Member] = commands.flag(name='member', positional=True, default=lambda ctx: []) + reason: Optional[str] = None + +.. note:: + Only one positional flag is allowed in a flag converter. + In order to customise the flag syntax we also have a few options that can be passed to the class parameter list: .. code-block:: python3 @@ -796,12 +809,17 @@ In order to customise the flag syntax we also have a few options that can be pas topic: Optional[str] nsfw: Optional[bool] slowmode: Optional[int] + + # Hello there --bold True + class Greeting(commands.FlagConverter): + text: str = commands.flag(positional=True) + bold: bool = False .. note:: Despite the similarities in these examples to command like arguments, the syntax and parser is not a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result - all flags need a corresponding value. + all flags need a corresponding value unless part of a positional flag. Flag converters will only raise :exc:`~ext.commands.FlagError` derived exceptions. If an error is raised while converting a flag, :exc:`~ext.commands.BadFlagArgument` is raised instead and the original exception diff --git a/docs/faq.rst b/docs/faq.rst index 0cd8b8ad6b8c..16d03362abef 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -439,7 +439,7 @@ How can I disable all items on timeout? This requires three steps. -1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :meth:`Interaction.original_response`. +1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :attr:`InteractionCallbackResponse.resource`. 2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled. 3. Edit the message we retrieved in step 1 with the newly modified view. @@ -467,7 +467,7 @@ Putting it all together, we can do this in a text command: # Step 1 view.message = await ctx.send('Press me!', view=view) -Application commands do not return a message when you respond with :meth:`InteractionResponse.send_message`, therefore in order to reliably do this we should retrieve the message using :meth:`Interaction.original_response`. +Application commands, when you respond with :meth:`InteractionResponse.send_message`, return an instance of :class:`InteractionCallbackResponse` which contains the message you sent. This is the message you should attach to the view. Putting it all together, using the previous view definition: @@ -477,10 +477,13 @@ Putting it all together, using the previous view definition: async def more_timeout_example(interaction): """Another example to showcase disabling buttons on timing out""" view = MyView() - await interaction.response.send_message('Press me!', view=view) + callback = await interaction.response.send_message('Press me!', view=view) # Step 1 - view.message = await interaction.original_response() + resource = callback.resource + # making sure it's an interaction response message + if isinstance(resource, discord.InteractionMessage): + view.message = resource Application Commands diff --git a/docs/intents.rst b/docs/intents.rst index e805c5ff71ec..ca85ab8ddc04 100644 --- a/docs/intents.rst +++ b/docs/intents.rst @@ -114,6 +114,7 @@ Message Content - Whether you use :attr:`Message.attachments` to check message attachments. - Whether you use :attr:`Message.embeds` to check message embeds. - Whether you use :attr:`Message.components` to check message components. +- Whether you use :attr:`Message.poll` to check the message polls. - Whether you use the commands extension with a non-mentioning prefix. .. _intents_member_cache: diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 95c1922d181d..b75d33044b71 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -28,6 +28,22 @@ InteractionResponse .. autoclass:: InteractionResponse() :members: +InteractionCallbackResponse +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallbackResponse + +.. autoclass:: InteractionCallbackResponse() + :members: + +InteractionCallbackActivityInstance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallbackActivityInstance + +.. autoclass:: InteractionCallbackActivityInstance() + :members: + InteractionMessage ~~~~~~~~~~~~~~~~~~~ @@ -45,6 +61,14 @@ MessageInteraction .. autoclass:: MessageInteraction() :members: +MessageInteractionMetadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MessageInteractionMetadata + +.. autoclass:: MessageInteractionMetadata() + :members: + Component ~~~~~~~~~~ @@ -89,6 +113,77 @@ TextInput :members: :inherited-members: + +SectionComponent +~~~~~~~~~~~~~~~~ + +.. attributetable:: SectionComponent + +.. autoclass:: SectionComponent() + :members: + :inherited-members: + + +ThumbnailComponent +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: ThumbnailComponent + +.. autoclass:: ThumbnailComponent() + :members: + :inherited-members: + + +TextDisplay +~~~~~~~~~~~ + +.. attributetable:: TextDisplay + +.. autoclass:: TextDisplay() + :members: + :inherited-members: + + +MediaGalleryComponent +~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MediaGalleryComponent + +.. autoclass:: MediaGalleryComponent() + :members: + :inherited-members: + + +FileComponent +~~~~~~~~~~~~~ + +.. attributetable:: FileComponent + +.. autoclass:: FileComponent() + :members: + :inherited-members: + + +SeparatorComponent +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SeparatorComponent + +.. autoclass:: SeparatorComponent() + :members: + :inherited-members: + + +Container +~~~~~~~~~ + +.. attributetable:: Container + +.. autoclass:: Container() + :members: + :inherited-members: + + AppCommand ~~~~~~~~~~~ @@ -129,6 +224,22 @@ AppCommandPermissions .. autoclass:: discord.app_commands.AppCommandPermissions() :members: +AppCommandContext +~~~~~~~~~~~~~~~~~ + +.. attributetable:: discord.app_commands.AppCommandContext + +.. autoclass:: discord.app_commands.AppCommandContext + :members: + +AppInstallationType +~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: discord.app_commands.AppInstallationType + +.. autoclass:: discord.app_commands.AppInstallationType + :members: + GuildAppCommandPermissions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -259,7 +370,7 @@ Enumerations .. attribute:: action_row - Represents the group component which holds different components in a row. + Represents a component which holds different components in a row. .. attribute:: button @@ -289,6 +400,38 @@ Enumerations Represents a select in which both users and roles can be selected. + .. attribute:: channel_select + + Represents a channel select component. + + .. attribute:: section + + Represents a component which holds different components in a section. + + .. attribute:: text_display + + Represents a text display component. + + .. attribute:: thumbnail + + Represents a thumbnail component. + + .. attribute:: media_gallery + + Represents a media gallery component. + + .. attribute:: file + + Represents a file component. + + .. attribute:: separator + + Represents a separator component. + + .. attribute:: container + + Represents a component which holds different components in a container. + .. class:: ButtonStyle Represents the style of the button component. @@ -310,7 +453,12 @@ Enumerations .. attribute:: link Represents a link button. + .. attribute:: premium + + Represents a button denoting that buying a SKU is + required to perform this action. + .. versionadded:: 2.4 .. attribute:: blurple An alias for :attr:`primary`. @@ -418,6 +566,19 @@ Enumerations The permission is for a user. +.. class:: SeparatorSize + + The separator's size type. + + .. versionadded:: 2.6 + + .. attribute:: small + + A small separator. + .. attribute:: large + + A large separator. + .. _discord_ui_kit: Bot UI Kit @@ -433,6 +594,7 @@ View .. autoclass:: discord.ui.View :members: + :inherited-members: Modal ~~~~~~ @@ -443,6 +605,15 @@ Modal :members: :inherited-members: +LayoutView +~~~~~~~~~~ + +.. attributetable:: discord.ui.LayoutView + +.. autoclass:: discord.ui.LayoutView + :members: + :inherited-members: + Item ~~~~~~~ @@ -537,6 +708,86 @@ TextInput :members: :inherited-members: + +Container +~~~~~~~~~ + +.. attributetable:: discord.ui.Container + +.. autoclass:: discord.ui.Container + :members: + :inherited-members: + + +File +~~~~ + +.. attributetable:: discord.ui.File + +.. autoclass:: discord.ui.File + :members: + :inherited-members: + + +MediaGallery +~~~~~~~~~~~~ + +.. attributetable:: discord.ui.MediaGallery + +.. autoclass:: discord.ui.MediaGallery + :members: + :inherited-members: + + +Section +~~~~~~~ + +.. attributetable:: discord.ui.Section + +.. autoclass:: discord.ui.Section + :members: + :inherited-members: + + +Separator +~~~~~~~~~ + +.. attributetable:: discord.ui.Separator + +.. autoclass:: discord.ui.Separator + :members: + :inherited-members: + + +TextDisplay +~~~~~~~~~~~ + +.. attributetable:: discord.ui.TextDisplay + +.. autoclass:: discord.ui.TextDisplay + :members: + :inherited-members: + + +Thumbnail +~~~~~~~~~ + +.. attributetable:: discord.ui.Thumbnail + +.. autoclass:: discord.ui.Thumbnail + :members: + :inherited-members: + + +ActionRow +~~~~~~~~~ + +.. attributetable:: discord.ui.ActionRow + +.. autoclass:: discord.ui.ActionRow + :members: + :inherited-members: + .. _discord_app_commands: Application Commands @@ -642,6 +893,24 @@ Decorators .. autofunction:: discord.app_commands.guild_only :decorator: +.. autofunction:: discord.app_commands.dm_only + :decorator: + +.. autofunction:: discord.app_commands.private_channel_only + :decorator: + +.. autofunction:: discord.app_commands.allowed_contexts + :decorator: + +.. autofunction:: discord.app_commands.user_install + :decorator: + +.. autofunction:: discord.app_commands.guild_install + :decorator: + +.. autofunction:: discord.app_commands.allowed_installs + :decorator: + .. autofunction:: discord.app_commands.default_permissions :decorator: @@ -825,9 +1094,6 @@ Exceptions .. autoexception:: discord.app_commands.CommandNotFound :members: -.. autoexception:: discord.app_commands.MissingApplicationID - :members: - .. autoexception:: discord.app_commands.CommandSyncFailure :members: @@ -852,7 +1118,7 @@ Exception Hierarchy - :exc:`~discord.app_commands.CommandAlreadyRegistered` - :exc:`~discord.app_commands.CommandSignatureMismatch` - :exc:`~discord.app_commands.CommandNotFound` - - :exc:`~discord.app_commands.MissingApplicationID` + - :exc:`~discord.MissingApplicationID` - :exc:`~discord.app_commands.CommandSyncFailure` - :exc:`~discord.HTTPException` - :exc:`~discord.app_commands.CommandSyncFailure` diff --git a/docs/locale/ja/LC_MESSAGES/api.po b/docs/locale/ja/LC_MESSAGES/api.po index c6f40a2ebe50..96a912b31695 100644 --- a/docs/locale/ja/LC_MESSAGES/api.po +++ b/docs/locale/ja/LC_MESSAGES/api.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -309,8 +309,8 @@ msgstr "これが ``__init__`` で渡されなかった場合、データを含 #: ../../../discord/client.py:docstring of discord.Client.application_id:10 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:72 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:82 -#: ../../api.rst:1431 -#: ../../../discord/audit_logs.py:docstring of discord.audit_logs.AuditLogEntry:41 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:7 +#: ../../api.rst:1484 msgid "Optional[:class:`int`]" msgstr "Optional[:class:`int`]" @@ -703,8 +703,8 @@ msgstr ":class:`.abc.GuildChannel` を受け取ったからと言って、その #: ../../../discord/client.py:docstring of discord.client.Client.get_all_channels:0 #: ../../../discord/client.py:docstring of discord.client.Client.get_all_members:0 #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:0 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:0 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:0 -#: ../../../discord/user.py:docstring of discord.abc.Messageable.history:0 msgid "Yields" msgstr "Yieldする値" @@ -750,9 +750,9 @@ msgstr "この関数は **条件を満たす最初のイベント** を返しま #: ../../../discord/client.py:docstring of discord.client.Client.wait_for:22 #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:14 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:6 #: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio.from_probe:7 #: ../../../discord/utils.py:docstring of discord.utils.get:24 -#: ../../../discord/abc.py:docstring of discord.abc.GuildChannel.set_permissions:25 msgid "Examples" msgstr "例" @@ -832,23 +832,24 @@ msgid "This method is an API call. For general usage, consider :attr:`guilds` in msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わりに使用してください。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:15 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:7 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:7 #: ../../../discord/user.py:docstring of discord.abc.Messageable.history:7 #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction.users:12 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_members:27 msgid "Usage ::" msgstr "使い方 ::" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:20 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:12 #: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:15 msgid "Flattening into a list ::" msgstr "リストへフラット化 ::" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:25 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:17 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:19 #: ../../../discord/user.py:docstring of discord.abc.Messageable.history:19 #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_members:10 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:20 msgid "All parameters are optional." msgstr "すべてのパラメータがオプションです。" @@ -930,9 +931,8 @@ msgid "Whether to include count information in the guild. This fills the :attr:` msgstr "ギルドにカウント情報を含めるかどうか。これを使うことで特権インテントがなくても :attr:`.Guild.approximate_member_count` と :attr:`.Guild.approximate_presence_count` 属性が設定されます。デフォルトは ``True`` です。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guild:28 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:16 -msgid "You do not have access to the guild." -msgstr "ギルドにアクセスする権限がない場合。" +msgid "The guild doesn't exist or you got no access to it." +msgstr "" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guild:29 msgid "Getting the guild failed." @@ -1216,7 +1216,10 @@ msgid "Invalid Channel ID." msgstr "引数が無効なチャンネル IDである場合。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_channel:18 +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:14 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:14 #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_channel:14 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:14 msgid "You do not have permission to fetch this channel." msgstr "このチャンネルからメッセージを取得する権限がない場合。" @@ -1278,6 +1281,123 @@ msgstr "要求されたスタンプ。" msgid "Union[:class:`.StandardSticker`, :class:`.GuildSticker`]" msgstr "Union[:class:`.StandardSticker`, :class:`.GuildSticker`]" +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:3 +msgid "Retrieves the bot's available SKUs." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:7 +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:11 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:39 +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:14 +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:5 +msgid "The application ID could not be found." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:8 +msgid "Retrieving the SKUs failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:10 +msgid "The bot's available SKUs." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:11 +msgid "List[:class:`.SKU`]" +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:3 +msgid "Retrieves a :class:`.Entitlement` with the specified ID." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:7 +msgid "The entitlement's ID to fetch from." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:10 +msgid "An entitlement with this ID does not exist." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:12 +msgid "Fetching the entitlement failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:14 +msgid "The entitlement you requested." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:15 +msgid ":class:`.Entitlement`" +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:1 +msgid "Retrieves an :term:`asynchronous iterator` of the :class:`.Entitlement` that applications has." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:19 +msgid "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``." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:22 +msgid "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." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:26 +msgid "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." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:30 +msgid "A list of SKUs to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:32 +msgid "The user to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:34 +msgid "The guild to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:36 +msgid "Whether to exclude ended entitlements. Defaults to ``False``." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:40 +msgid "Fetching the entitlements failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:41 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:33 +msgid "Both ``after`` and ``before`` were provided, as Discord does not support this type of pagination." +msgstr "``after`` と ``before`` の両方が渡された場合。Discordはこのタイプのページネーションをサポートしていません。" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:43 +msgid ":class:`.Entitlement` -- The entitlement with the application." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:3 +msgid "Creates a test :class:`.Entitlement` for the application." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:7 +msgid "The SKU to create the entitlement for." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:9 +msgid "The ID of the owner." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:11 +msgid "The type of the owner." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:15 +msgid "The SKU or owner could not be found." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:16 +msgid "Creating the entitlement failed." +msgstr "" + #: ../../../discord/client.py:docstring of discord.client.Client.fetch_premium_sticker_packs:3 msgid "Retrieves all available premium sticker packs." msgstr "利用可能なプレミアムスタンプパックをすべて取得します。" @@ -1322,6 +1442,32 @@ msgstr "作成されたチャンネル。" msgid ":class:`.DMChannel`" msgstr ":class:`.DMChannel`" +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:1 +msgid "Registers :class:`~discord.ui.DynamicItem` classes for persistent listening." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:3 +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:3 +msgid "This method accepts *class types* rather than instances." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:7 +msgid "The classes of dynamic items to add." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:10 +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:10 +msgid "A class is not a subclass of :class:`~discord.ui.DynamicItem`." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:1 +msgid "Removes :class:`~discord.ui.DynamicItem` classes from persistent listening." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:7 +msgid "The classes of dynamic items to remove." +msgstr "" + #: ../../../discord/client.py:docstring of discord.client.Client.add_view:1 msgid "Registers a :class:`~discord.ui.View` for persistent listening." msgstr ":class:`~discord.ui.View` を永続的にインタラクションを受け取るために登録します。" @@ -1386,6 +1532,15 @@ msgstr "シャードの起動時に利用するshard_idsのオプショナルな msgid "Optional[List[:class:`int`]]" msgstr "Optional[List[:class:`int`]]" +#: ../../../discord/shard.py:docstring of discord.shard.AutoShardedClient:37 +msgid "The maximum number of seconds to wait before timing out when launching a shard. Defaults to 180 seconds." +msgstr "" + +#: ../../../discord/shard.py:docstring of discord.shard.AutoShardedClient:42 +#: ../../../discord/message.py:docstring of discord.message.Attachment:99 +msgid "Optional[:class:`float`]" +msgstr "Optional[:class:`float`]" + #: ../../../discord/shard.py:docstring of discord.AutoShardedClient.latency:3 msgid "This operates similarly to :meth:`Client.latency` except it uses the average latency of every shard's latency. To get a list of shard latency, check the :attr:`latencies` property. Returns ``nan`` if there are no shards ready." msgstr "これは :meth:`Client.latency` と同様に機能しますが、すべてのシャードの平均待ち時間を使用する点が異なります。シャードの待ち時間のリストを取得するには :attr:`latencies` プロパティを参照してください。準備ができていない場合は ``nan`` を返します。" @@ -1490,7 +1645,7 @@ msgid "The application owner." msgstr "アプリケーションの所有者。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:20 -#: ../../api.rst:4277 +#: ../../api.rst:4492 #: ../../../discord/integrations.py:docstring of discord.integrations.Integration:45 #: ../../../discord/integrations.py:docstring of discord.integrations.BotIntegration:39 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration:63 @@ -1558,7 +1713,7 @@ msgstr "このアプリケーションがDiscord上で販売されているゲ #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:99 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:107 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:140 -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:40 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:149 msgid "Optional[:class:`str`]" msgstr "Optional[:class:`str`]" @@ -1578,9 +1733,9 @@ msgstr "アプリケーションの機能を説明するタグのリスト。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:115 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:123 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:157 #: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:62 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInstallParams:10 -#: ../../../discord/guild.py:docstring of discord.guild.Guild:133 msgid "List[:class:`str`]" msgstr "List[:class:`str`]" @@ -1601,6 +1756,16 @@ msgstr "Optional[:class:`AppInstallParams`]" msgid "The application's connection verification URL which will render the application as a verification method in the guild's role verification configuration." msgstr "アプリケーションをギルドのロール紐づけ設定にて紐づけ方法として扱うようにするための、アプリケーションの接続確認URL。" +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:144 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:66 +msgid "The interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway, if configured." +msgstr "設定されている場合、ゲートウェイではなくエンドポイントからインタラクションを受け取るアプリケーションの、インタラクションエンドポイントのURI。" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:153 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:58 +msgid "A list of authentication redirect URIs." +msgstr "認証リダイレクトURIのリスト。" + #: ../../../discord/appinfo.py:docstring of discord.AppInfo.icon:1 #: ../../../discord/appinfo.py:docstring of discord.PartialAppInfo.icon:1 msgid "Retrieves the application's icon asset, if any." @@ -1645,6 +1810,74 @@ msgstr "アプリケーションのフラグ。" msgid ":class:`ApplicationFlags`" msgstr ":class:`ApplicationFlags`" +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:3 +msgid "Edits the application info." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:7 +msgid "The new custom authorization URL for the application. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:9 +msgid "The new application description. Can be ``None`` to remove the description." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:11 +msgid "The new application’s connection verification URL which will render the application as a verification method in the guild’s role verification configuration. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:14 +msgid "The new list of :ddocs:`OAuth2 scopes ` of the :attr:`~install_params`. Can be ``None`` to remove the scopes." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:17 +msgid "The new permissions of the :attr:`~install_params`. Can be ``None`` to remove the permissions." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:19 +msgid "The new application’s flags. Only limited intent flags (:attr:`~ApplicationFlags.gateway_presence_limited`, :attr:`~ApplicationFlags.gateway_guild_members_limited`, :attr:`~ApplicationFlags.gateway_message_content_limited`) can be edited. Can be ``None`` to remove the flags." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:25 +msgid "Editing the limited intent flags leads to the termination of the bot." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:27 +msgid "The new application’s icon as a :term:`py:bytes-like object`. Can be ``None`` to remove the icon." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:29 +msgid "The new application’s cover image as a :term:`py:bytes-like object` on a store embed. The cover image is only available if the application is a game sold on Discord. Can be ``None`` to remove the image." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:33 +msgid "The new interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:36 +msgid "The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:38 +msgid "The reason for editing the application. Shows up on the audit log." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:41 +msgid "Editing the application failed" +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:42 +msgid "The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:44 +msgid "The newly updated application info." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:45 +msgid ":class:`AppInfo`" +msgstr "" + #: ../../api.rst:68 msgid "PartialAppInfo" msgstr "PartialAppInfo" @@ -1657,14 +1890,6 @@ msgstr ":func:`~discord.abc.GuildChannel.create_invite` により与えられた msgid "The approximate count of the guilds the bot was added to." msgstr "ボットが追加されたギルドのおおよその数。" -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:58 -msgid "A list of authentication redirect URIs." -msgstr "認証リダイレクトURIのリスト。" - -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:66 -msgid "The interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway, if configured." -msgstr "設定されている場合、ゲートウェイではなくエンドポイントからインタラクションを受け取るアプリケーションの、インタラクションエンドポイントのURI。" - #: ../../../discord/appinfo.py:docstring of discord.PartialAppInfo.cover_image:1 msgid "Retrieves the cover image of the application's default rich presence." msgstr "存在する場合は、アプリケーションの既定のリッチプレゼンスのカバー画像を取得します。" @@ -1686,8 +1911,8 @@ msgid "The permissions to give to application in the guild." msgstr "ギルドに追加するアプリケーションに与える権限。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInstallParams:16 -#: ../../api.rst:3659 -#: ../../api.rst:3748 +#: ../../api.rst:3868 +#: ../../api.rst:3957 #: ../../../discord/member.py:docstring of discord.Member.guild_permissions:14 #: ../../../discord/role.py:docstring of discord.Role.permissions:3 msgid ":class:`Permissions`" @@ -1795,6 +2020,14 @@ msgstr "メンバーの参加状態 (例:招待されたか、承認された msgid ":class:`TeamMembershipState`" msgstr ":class:`TeamMembershipState`" +#: ../../../discord/team.py:docstring of discord.team.TeamMember:69 +msgid "The role of the member within the team." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.team.TeamMember:73 +msgid ":class:`TeamMemberRole`" +msgstr "" + #: ../../../discord/team.py:docstring of discord.TeamMember.accent_color:1 #: ../../../discord/user.py:docstring of discord.ClientUser.accent_color:1 #: ../../../discord/user.py:docstring of discord.User.accent_color:1 @@ -1866,13 +2099,35 @@ msgid "If the user has not uploaded a global avatar, ``None`` is returned. If yo msgstr "ユーザーがグローバルのアバターをアップロードしていない場合は、 ``None`` が返されます。ユーザーが表示しているアバターを取得したい場合は、 :attr:`display_avatar` を検討してください。" #: ../../../discord/team.py:docstring of discord.TeamMember.avatar:6 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:7 #: ../../../discord/team.py:docstring of discord.TeamMember.banner:9 #: ../../../discord/webhook/async_.py:docstring of discord.Webhook.avatar:6 #: ../../../discord/webhook/sync.py:docstring of discord.SyncWebhook.avatar:6 -#: ../../../discord/user.py:docstring of discord.ClientUser.avatar:6 msgid "Optional[:class:`Asset`]" msgstr "Optional[:class:`Asset`]" +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:1 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration:1 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration:1 +#: ../../../discord/widget.py:docstring of discord.WidgetMember.avatar_decoration:1 +msgid "Returns an :class:`Asset` for the avatar decoration the user has." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:3 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:3 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration:3 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration_sku_id:3 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration:3 +msgid "If the user has not set an avatar decoration, ``None`` is returned." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:1 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration_sku_id:1 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration_sku_id:1 +#: ../../../discord/widget.py:docstring of discord.WidgetMember.avatar_decoration_sku_id:1 +msgid "Returns the SKU ID of the avatar decoration the user has." +msgstr "" + #: ../../../discord/team.py:docstring of discord.TeamMember.banner:1 #: ../../../discord/user.py:docstring of discord.ClientUser.banner:1 #: ../../../discord/user.py:docstring of discord.User.banner:1 @@ -1897,7 +2152,7 @@ msgstr ":attr:`colour` という名前のエイリアスが存在します。" #: ../../../discord/team.py:docstring of discord.TeamMember.color:6 #: ../../../discord/team.py:docstring of discord.TeamMember.colour:6 -#: ../../api.rst:3668 +#: ../../api.rst:3877 #: ../../../discord/user.py:docstring of discord.ClientUser.color:6 #: ../../../discord/user.py:docstring of discord.ClientUser.colour:6 msgid ":class:`Colour`" @@ -1950,9 +2205,9 @@ msgstr "ユーザーの既定のアバターを返します。" #: ../../../discord/team.py:docstring of discord.TeamMember.default_avatar:3 #: ../../../discord/team.py:docstring of discord.TeamMember.display_avatar:7 -#: ../../api.rst:3442 -#: ../../api.rst:3448 -#: ../../api.rst:3454 +#: ../../api.rst:3651 +#: ../../api.rst:3657 +#: ../../api.rst:3663 msgid ":class:`Asset`" msgstr ":class:`Asset`" @@ -2079,7 +2334,7 @@ msgstr "接続しているギルド。" #: ../../../discord/voice_client.py:docstring of discord.VoiceClient.guild:3 #: ../../../discord/audit_logs.py:docstring of discord.audit_logs.AuditLogEntry:53 -#: ../../api.rst:3436 +#: ../../api.rst:3645 #: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:15 #: ../../../discord/automod.py:docstring of discord.AutoModAction.guild:3 msgid ":class:`Guild`" @@ -2090,7 +2345,7 @@ msgid "The user connected to voice (i.e. ourselves)." msgstr "ボイスチャンネルに接続しているユーザー。(つまり、自分自身)" #: ../../../discord/voice_client.py:docstring of discord.VoiceClient.user:3 -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:31 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:37 #: ../../../discord/channel.py:docstring of discord.channel.DMChannel:33 #: ../../../discord/channel.py:docstring of discord.channel.GroupChannel:31 msgid ":class:`ClientUser`" @@ -2120,6 +2375,14 @@ msgstr "別のボイスチャンネルへ移動させます。" msgid "The channel to move to. Must be a voice channel." msgstr "移動先のチャンネル。ボイスチャンネルである必要があります。" +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.move_to:7 +msgid "How long to wait for the move to complete." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.move_to:12 +msgid "The move did not complete in time, but may still be ongoing." +msgstr "" + #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.is_connected:1 msgid "Indicates if the voice client is connected to voice." msgstr "ボイスチャンネルに接続しているかどうか。" @@ -2137,29 +2400,65 @@ msgid "If an error happens while the audio player is running, the exception is c msgstr "オーディオプレーヤーの実行中にエラーが発生した場合、例外が捕捉され、オーディオプレーヤーが停止します。 コールバックが渡されない場合、捕捉された例外はライブラリロガーを用いて記録されます。" #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:10 +msgid "Extra parameters may be passed to the internal opus encoder if a PCM based source is used. Otherwise, they are ignored." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:13 msgid "Instead of writing to ``sys.stderr``, the library's logger is used." msgstr "``sys.stderr`` に出力するのではなく、ライブラリロガーが使用されるようになりました。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:13 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:16 +msgid "Added encoder parameters as keyword arguments." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:19 msgid "The audio source we're reading from." msgstr "読み込むオーディオソース。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:15 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:21 msgid "The finalizer that is called after the stream is exhausted. This function must have a single parameter, ``error``, that denotes an optional exception that was raised during playing." msgstr "ファイナライザーはストリームが空になると呼び出されます。この関数には再生中に発生したオプションの例外を表す一つのパラメータ ``error`` が必要です。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:20 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:25 +msgid "Configures the encoder's intended application. Can be one of: ``'audio'``, ``'voip'``, ``'lowdelay'``. Defaults to ``'audio'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:29 +msgid "Configures the bitrate in the encoder. Can be between ``16`` and ``512``. Defaults to ``128``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:32 +msgid "Configures the encoder's use of inband forward error correction. Defaults to ``True``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:35 +msgid "Configures the encoder's expected packet loss percentage. Requires FEC. Defaults to ``0.15``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:38 +msgid "Configures the encoder's bandpass. Can be one of: ``'narrow'``, ``'medium'``, ``'wide'``, ``'superwide'``, ``'full'``. Defaults to ``'full'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:42 +msgid "Configures the type of signal being encoded. Can be one of: ``'auto'``, ``'voice'``, ``'music'``. Defaults to ``'auto'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:47 msgid "Already playing audio or not connected." msgstr "既にオーディオを再生しているか、接続されていない場合。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:21 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:48 msgid "Source is not a :class:`AudioSource` or after is not a callable." msgstr "ソースが :class:`AudioSource` でないか、afterが呼び出し可能でない場合。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:22 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:49 msgid "Source is not opus encoded and opus is not loaded." msgstr "ソースがopusエンコードされておらず、opusが読み込まれていない場合。" +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:50 +msgid "An improper value was passed as an encoder parameter." +msgstr "" + #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.is_playing:1 msgid "Indicates if we're currently playing audio." msgstr "現在オーディオを再生しているか。" @@ -2248,7 +2547,11 @@ msgstr "接続されているボイスチャンネル。" msgid "An abstract method that is called when the client's voice state has changed. This corresponds to ``VOICE_STATE_UPDATE``." msgstr "クライアントの音声状態が変更された際に呼び出される抽象メソッドです。これは ``VOICE_STATE_UPDATE`` と対応しています。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:6 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:8 +msgid "This method is not the same as the event. See: :func:`on_voice_state_update`" +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:10 msgid "The raw :ddocs:`voice state payload `." msgstr "生の :ddocs:`ボイスステートペイロード ` 。" @@ -2457,28 +2760,33 @@ msgstr "ffmpegが受け取り、PCMバイトへ変換する入力。 ``pipe`` msgid "The executable name (and path) to use. Defaults to ``ffmpeg``." msgstr "使用する実行可能ファイルの名前 (およびパス)。デフォルトでは ``ffmpeg`` です。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:16 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:41 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:18 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:43 +msgid "Since this class spawns a subprocess, care should be taken to not pass in an arbitrary executable name when using this parameter." +msgstr "" + +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:21 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:46 msgid "If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``." msgstr "``True`` の場合、 ``source`` パラメータがffmpegの標準入力に渡されます。デフォルトでは ``False`` です。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:19 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:44 -msgid "A file-like object to pass to the Popen constructor. Could also be an instance of ``subprocess.PIPE``." -msgstr "Popenのコンストラクタに渡すファイルライクオブジェクト。 ``subprocess.PIPE`` のようなインスタンスにすることも可能です。" +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:24 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:49 +msgid "A file-like object to pass to the Popen constructor." +msgstr "" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:22 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:47 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:26 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:51 msgid "Extra command line arguments to pass to ffmpeg before the ``-i`` flag." msgstr "``-i`` フラグのまえにffmepgに渡す追加のコマンドライン引数。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:24 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:49 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:28 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:53 msgid "Extra command line arguments to pass to ffmpeg after the ``-i`` flag." msgstr "``-i`` フラグのあとにffmepgに渡す追加のコマンドライン引数。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:27 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:52 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:31 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:56 msgid "The subprocess failed to be created." msgstr "サブプロセスを作成できなかった場合。" @@ -2690,9 +2998,9 @@ msgstr "アプリケーションコマンドの権限が更新されたときに #: ../../api.rst:215 #: ../../api.rst:370 -#: ../../api.rst:736 -#: ../../api.rst:794 -#: ../../api.rst:979 +#: ../../api.rst:777 +#: ../../api.rst:835 +#: ../../api.rst:1020 msgid "The raw event payload data." msgstr "生のイベントペイロードデータ。" @@ -2709,7 +3017,7 @@ msgid "The command that completed successfully" msgstr "正常に実行されたコマンド。" #: ../../api.rst:231 -#: ../../api.rst:4201 +#: ../../api.rst:4416 msgid "AutoMod" msgstr "AutoMod" @@ -2770,8 +3078,8 @@ msgstr "ギルドは :attr:`~abc.GuildChannel.guild` で取得できます。" #: ../../api.rst:291 #: ../../api.rst:300 #: ../../api.rst:311 -#: ../../api.rst:546 -#: ../../api.rst:555 +#: ../../api.rst:587 +#: ../../api.rst:596 msgid "This requires :attr:`Intents.guilds` to be enabled." msgstr ":attr:`Intents.guilds` を有効にする必要があります。" @@ -2809,10 +3117,10 @@ msgid "Called whenever a private group DM is updated. e.g. changed name or topic msgstr "プライベートグループDMが更新されたとき呼び出されます。 例: 名前やトピックの変更。" #: ../../api.rst:322 -#: ../../api.rst:885 -#: ../../api.rst:919 -#: ../../api.rst:936 -#: ../../api.rst:953 +#: ../../api.rst:926 +#: ../../api.rst:960 +#: ../../api.rst:977 +#: ../../api.rst:994 msgid "This requires :attr:`Intents.messages` to be enabled." msgstr ":attr:`Intents.messages` を有効にする必要があります。" @@ -2989,191 +3297,231 @@ msgid "The message that is about to be passed on to the WebSocket library. It ca msgstr "WebSocketライブラリから渡されるメッセージ。バイナリメッセージの場合は :class:`bytes` 、通常のメッセージの場合は :class:`str` です。" #: ../../api.rst:500 +msgid "Entitlements" +msgstr "" + +#: ../../api.rst:504 +msgid "Called when a user subscribes to a SKU." +msgstr "" + +#: ../../api.rst:508 +msgid "The entitlement that was created." +msgstr "" + +#: ../../api.rst:513 +msgid "Called when a user updates their subscription to a SKU. This is usually called when the user renews or cancels their subscription." +msgstr "" + +#: ../../api.rst:518 +msgid "The entitlement that was updated." +msgstr "" + +#: ../../api.rst:523 +msgid "Called when a users subscription to a SKU is cancelled. This is typically only called when:" +msgstr "" + +#: ../../api.rst:525 +msgid "Discord issues a refund for the subscription." +msgstr "" + +#: ../../api.rst:526 +msgid "Discord removes an entitlement from a user." +msgstr "" + +#: ../../api.rst:530 +msgid "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." +msgstr "" + +#: ../../api.rst:536 +msgid "The entitlement that was deleted." +msgstr "" + +#: ../../api.rst:541 msgid "Gateway" msgstr "Gateway" -#: ../../api.rst:504 +#: ../../api.rst:545 msgid "Called when the client is done preparing the data received from Discord. Usually after login is successful and the :attr:`Client.guilds` and co. are filled up." msgstr "クライアントがDiscordから受信したデータの準備を完了した際に呼び出されます。通常はログインが成功したあと、 :attr:`Client.guilds` とそれに関連するものの準備が完了したときです。" -#: ../../api.rst:509 +#: ../../api.rst:550 msgid "This function is not guaranteed to be the first event called. Likewise, this function is **not** guaranteed to only be called once. This library implements reconnection logic and thus will end up calling this event whenever a RESUME request fails." msgstr "このイベントは、最初に呼び出されるイベントとは限りません。同時に、このイベントは **一度だけ呼ばれるという保証もできません** 。このライブラリは、再接続ロジックを実装しているためリジューム要求が失敗するたびにこのイベントが呼び出されることになります。" -#: ../../api.rst:516 +#: ../../api.rst:557 msgid "Called when the client has resumed a session." msgstr "クライアントがセッションを再開したときに呼び出されます。" -#: ../../api.rst:520 +#: ../../api.rst:561 msgid "Similar to :func:`on_ready` except used by :class:`AutoShardedClient` to denote when a particular shard ID has become ready." msgstr "特定の Shard IDが準備完了になったかを確認するために :class:`AutoShardedClient` で使用される以外は :func:`on_ready` とほとんど同じです。" -#: ../../api.rst:523 +#: ../../api.rst:564 msgid "The shard ID that is ready." msgstr "準備が完了したShard ID。" -#: ../../api.rst:529 +#: ../../api.rst:570 msgid "Similar to :func:`on_resumed` except used by :class:`AutoShardedClient` to denote when a particular shard ID has resumed a session." msgstr "特定のシャードIDを持つシャードがセッションを再開したかどうかを確認するために :class:`AutoShardedClient` で使用されることを除けば :func:`on_resumed` とほとんど同じです。" -#: ../../api.rst:534 +#: ../../api.rst:575 msgid "The shard ID that has resumed." msgstr "セッションが再開したシャードのID。" -#: ../../api.rst:538 +#: ../../api.rst:579 msgid "Guilds" msgstr "Guilds" -#: ../../api.rst:543 +#: ../../api.rst:584 msgid "Called when a guild becomes available or unavailable. The guild must have existed in the :attr:`Client.guilds` cache." msgstr "ギルドが利用可能・不可能になったときに呼び出されます。ギルドは :attr:`Client.guilds` キャッシュに存在していないといけません。" -#: ../../api.rst:548 +#: ../../api.rst:589 msgid "The :class:`Guild` that has changed availability." msgstr "利用状況が変わった :class:`Guild` 。" -#: ../../api.rst:552 +#: ../../api.rst:593 msgid "Called when a :class:`Guild` is either created by the :class:`Client` or when the :class:`Client` joins a guild." msgstr ":class:`Client` によって :class:`Guild` が作成された。または :class:`Client` がギルドに参加したときに呼び出されます。" -#: ../../api.rst:557 +#: ../../api.rst:598 msgid "The guild that was joined." msgstr "参加したギルド。" -#: ../../api.rst:562 +#: ../../api.rst:603 msgid "Called when a :class:`Guild` is removed from the :class:`Client`." msgstr ":class:`Client` が :class:`Guild` から削除されたときに呼び出されます。" -#: ../../api.rst:564 +#: ../../api.rst:605 msgid "This happens through, but not limited to, these circumstances:" msgstr "これは以下の状況時に呼び出されますが、これに限ったものではありません:" -#: ../../api.rst:566 +#: ../../api.rst:607 msgid "The client got banned." msgstr "クライアントがBANされた。" -#: ../../api.rst:567 +#: ../../api.rst:608 msgid "The client got kicked." msgstr "クライアントがキックされた。" -#: ../../api.rst:568 +#: ../../api.rst:609 msgid "The client left the guild." msgstr "クライアントがギルドから脱退した。" -#: ../../api.rst:569 +#: ../../api.rst:610 msgid "The client or the guild owner deleted the guild." msgstr "クライアント、またはギルドオーナーがギルドを削除した。" -#: ../../api.rst:571 +#: ../../api.rst:612 msgid "In order for this event to be invoked then the :class:`Client` must have been part of the guild to begin with. (i.e. it is part of :attr:`Client.guilds`)" msgstr "このイベントが呼び出されるためには、 :class:`Client` がギルドに参加している必要があります。(つまり、 :attr:`Client.guilds` にギルドが存在しなければならない)" -#: ../../api.rst:576 +#: ../../api.rst:617 msgid "The guild that got removed." msgstr "削除されたギルド。" -#: ../../api.rst:581 +#: ../../api.rst:622 msgid "Called when a :class:`Guild` updates, for example:" msgstr ":class:`Guild` が更新されたときに呼び出されます。例えば:" -#: ../../api.rst:583 +#: ../../api.rst:624 msgid "Changed name" msgstr "名前が変更された" -#: ../../api.rst:584 +#: ../../api.rst:625 msgid "Changed AFK channel" msgstr "AFKチャンネルが変更された" -#: ../../api.rst:585 +#: ../../api.rst:626 msgid "Changed AFK timeout" msgstr "AFKのタイムアウト時間が変更された" -#: ../../api.rst:586 +#: ../../api.rst:627 msgid "etc" msgstr "その他" -#: ../../api.rst:590 +#: ../../api.rst:631 msgid "The guild prior to being updated." msgstr "更新される前のギルド。" -#: ../../api.rst:592 +#: ../../api.rst:633 msgid "The guild after being updated." msgstr "更新された後のギルド。" -#: ../../api.rst:597 +#: ../../api.rst:638 msgid "Called when a :class:`Guild` adds or removes :class:`Emoji`." msgstr ":class:`Guild` に :class:`Emoji` が追加、または削除されたときに呼び出されます。" -#: ../../api.rst:599 -#: ../../api.rst:612 +#: ../../api.rst:640 +#: ../../api.rst:653 msgid "This requires :attr:`Intents.emojis_and_stickers` to be enabled." msgstr ":attr:`Intents.emojis_and_stickers` を有効にする必要があります。" -#: ../../api.rst:601 +#: ../../api.rst:642 msgid "The guild who got their emojis updated." msgstr "絵文字が更新されたギルド。" -#: ../../api.rst:603 +#: ../../api.rst:644 msgid "A list of emojis before the update." msgstr "更新前の絵文字のリスト。" -#: ../../api.rst:605 +#: ../../api.rst:646 msgid "A list of emojis after the update." msgstr "更新後の絵文字のリスト。" -#: ../../api.rst:610 +#: ../../api.rst:651 msgid "Called when a :class:`Guild` updates its stickers." msgstr ":class:`Guild` のスタンプが更新されたときに呼び出されます。" -#: ../../api.rst:616 +#: ../../api.rst:657 msgid "The guild who got their stickers updated." msgstr "スタンプが更新されたギルド。" -#: ../../api.rst:618 +#: ../../api.rst:659 msgid "A list of stickers before the update." msgstr "更新前のスタンプのリスト。" -#: ../../api.rst:620 +#: ../../api.rst:661 msgid "A list of stickers after the update." msgstr "更新後のスタンプのリスト。" -#: ../../api.rst:625 +#: ../../api.rst:666 msgid "Called when a :class:`Guild` gets a new audit log entry. You must have :attr:`~Permissions.view_audit_log` to receive this." msgstr ":class:`Guild` に新しい監査ログ項目が追加されたときに呼び出されます。これを受け取るには :attr:`~Permissions.view_audit_log` が必要です。" -#: ../../api.rst:628 -#: ../../api.rst:840 -#: ../../api.rst:853 +#: ../../api.rst:669 +#: ../../api.rst:881 +#: ../../api.rst:894 msgid "This requires :attr:`Intents.moderation` to be enabled." msgstr ":attr:`Intents.moderation` を有効にする必要があります。" -#: ../../api.rst:634 +#: ../../api.rst:675 msgid "Audit log entries received through the gateway are subject to data retrieval from cache rather than REST. This means that some data might not be present when you expect it to be. For example, the :attr:`AuditLogEntry.target` attribute will usually be a :class:`discord.Object` and the :attr:`AuditLogEntry.user` attribute will depend on user and member cache." msgstr "ゲートウェイ経由で取得した監査ログ項目はデータをRESTではなくキャッシュから取得します。このため、一部のデータが不足している場合があります。例えば、 :attr:`AuditLogEntry.target` 属性は多くの場合 :class:`discord.Object` になり、 :attr:`AuditLogEntry.user` 属性はユーザーとメンバーキャッシュに依存します。" -#: ../../api.rst:640 +#: ../../api.rst:681 msgid "To get the user ID of entry, :attr:`AuditLogEntry.user_id` can be used instead." msgstr "項目のユーザーIDを取得するには、代わりに :attr:`AuditLogEntry.user_id` を使用できます。" -#: ../../api.rst:642 +#: ../../api.rst:683 msgid "The audit log entry that was created." msgstr "作成された監査ログの項目。" -#: ../../api.rst:647 +#: ../../api.rst:688 msgid "Called when an :class:`Invite` is created. You must have :attr:`~Permissions.manage_channels` to receive this." msgstr ":class:`Invite` が作成されたときに呼び出されます。 受け取るには :attr:`~Permissions.manage_channels` が必要です。" -#: ../../api.rst:654 -#: ../../api.rst:671 +#: ../../api.rst:695 +#: ../../api.rst:712 msgid "There is a rare possibility that the :attr:`Invite.guild` and :attr:`Invite.channel` attributes will be of :class:`Object` rather than the respective models." msgstr "まれに :attr:`Invite.guild` と :attr:`Invite.channel` 属性がそれぞれの本来のモデルではなく :class:`Object` になることがあります。" -#: ../../api.rst:657 -#: ../../api.rst:677 +#: ../../api.rst:698 +#: ../../api.rst:718 msgid "This requires :attr:`Intents.invites` to be enabled." msgstr ":attr:`Intents.invites` を有効にする必要があります。" -#: ../../api.rst:659 +#: ../../api.rst:700 #: ../../../discord/abc.py:docstring of discord.abc.GuildChannel.create_invite:37 #: ../../../discord/channel.py:docstring of discord.abc.GuildChannel.create_invite:37 #: ../../../discord/channel.py:docstring of discord.abc.GuildChannel.create_invite:37 @@ -3181,704 +3529,712 @@ msgstr ":attr:`Intents.invites` を有効にする必要があります。" msgid "The invite that was created." msgstr "作成された招待。" -#: ../../api.rst:664 +#: ../../api.rst:705 msgid "Called when an :class:`Invite` is deleted. You must have :attr:`~Permissions.manage_channels` to receive this." msgstr ":class:`Invite` が削除されたときに呼び出されます。 受け取るには :attr:`~Permissions.manage_channels` が必要です。" -#: ../../api.rst:674 +#: ../../api.rst:715 msgid "Outside of those two attributes, the only other attribute guaranteed to be filled by the Discord gateway for this event is :attr:`Invite.code`." msgstr "これらの属性以外では、Discordゲートウェイによってこのイベントに与えられているのが保証されている属性は :attr:`Invite.code` のみです。" -#: ../../api.rst:679 +#: ../../api.rst:720 msgid "The invite that was deleted." msgstr "削除された招待。" -#: ../../api.rst:684 +#: ../../api.rst:725 msgid "Integrations" msgstr "Integrations" -#: ../../api.rst:688 +#: ../../api.rst:729 msgid "Called when an integration is created." msgstr "連携サービスが作成されたときに呼び出されます。" -#: ../../api.rst:690 -#: ../../api.rst:701 -#: ../../api.rst:712 -#: ../../api.rst:732 +#: ../../api.rst:731 +#: ../../api.rst:742 +#: ../../api.rst:753 +#: ../../api.rst:773 msgid "This requires :attr:`Intents.integrations` to be enabled." msgstr ":attr:`Intents.integrations` を有効にする必要があります。" -#: ../../api.rst:694 +#: ../../api.rst:735 msgid "The integration that was created." msgstr "作成された連携サービス。" -#: ../../api.rst:699 +#: ../../api.rst:740 msgid "Called when an integration is updated." msgstr "連携サービスが更新されたときに呼び出されます。" -#: ../../api.rst:705 +#: ../../api.rst:746 msgid "The integration that was updated." msgstr "更新された連携サービス。" -#: ../../api.rst:710 +#: ../../api.rst:751 msgid "Called whenever an integration is created, modified, or removed from a guild." msgstr "ギルドの連携サービスが作成、更新、削除されるたびに呼び出されます。" -#: ../../api.rst:716 +#: ../../api.rst:757 msgid "The guild that had its integrations updated." msgstr "連携サービスが更新されたギルド。" -#: ../../api.rst:721 +#: ../../api.rst:762 msgid "Called whenever a webhook is created, modified, or removed from a guild channel." msgstr "ギルドチャンネルのWebhookが作成、更新、削除されたときに呼び出されます。" -#: ../../api.rst:723 +#: ../../api.rst:764 msgid "This requires :attr:`Intents.webhooks` to be enabled." msgstr ":attr:`Intents.webhooks` を有効にする必要があります。" -#: ../../api.rst:725 +#: ../../api.rst:766 msgid "The channel that had its webhooks updated." msgstr "Webhookが更新されたチャンネル。" -#: ../../api.rst:730 +#: ../../api.rst:771 msgid "Called when an integration is deleted." msgstr "連携サービスが削除されたときに呼び出されます。" -#: ../../api.rst:740 +#: ../../api.rst:781 msgid "Interactions" msgstr "Interactions" -#: ../../api.rst:744 +#: ../../api.rst:785 msgid "Called when an interaction happened." msgstr "インタラクションが発生したときに呼び出されます。" -#: ../../api.rst:746 +#: ../../api.rst:787 msgid "This currently happens due to slash command invocations or components being used." msgstr "これは、現在はスラッシュコマンドの呼び出しやコンポーネントの使用により起こります。" -#: ../../api.rst:750 +#: ../../api.rst:791 msgid "This is a low level function that is not generally meant to be used. If you are working with components, consider using the callbacks associated with the :class:`~discord.ui.View` instead as it provides a nicer user experience." msgstr "これは、一般的な使用を意図していない低レベル関数です。コンポーネントを使用している場合は、よりよいユーザーエクスペリエンスを提供する :class:`~discord.ui.View` のコールバックの使用を検討してください。" -#: ../../api.rst:756 +#: ../../api.rst:797 msgid "The interaction data." msgstr "インタラクションデータ。" -#: ../../api.rst:760 +#: ../../api.rst:801 msgid "Members" msgstr "Members" -#: ../../api.rst:764 +#: ../../api.rst:805 msgid "Called when a :class:`Member` joins a :class:`Guild`." msgstr ":class:`Member` が :class:`Guild` に参加したときに呼び出されます。" -#: ../../api.rst:766 -#: ../../api.rst:778 -#: ../../api.rst:790 -#: ../../api.rst:812 -#: ../../api.rst:829 +#: ../../api.rst:807 +#: ../../api.rst:819 +#: ../../api.rst:831 +#: ../../api.rst:853 +#: ../../api.rst:870 msgid "This requires :attr:`Intents.members` to be enabled." msgstr ":attr:`Intents.members` を有効にする必要があります。" -#: ../../api.rst:768 +#: ../../api.rst:809 msgid "The member who joined." msgstr "参加したメンバー。" -#: ../../api.rst:773 -#: ../../api.rst:785 +#: ../../api.rst:814 +#: ../../api.rst:826 msgid "Called when a :class:`Member` leaves a :class:`Guild`." msgstr ":class:`Member` が :class:`Guild` から脱退したときに呼び出されます。" -#: ../../api.rst:775 +#: ../../api.rst:816 msgid "If the guild or member could not be found in the internal cache this event will not be called, you may use :func:`on_raw_member_remove` instead." msgstr "ギルドまたはメンバーが内部キャッシュで見つからない場合、このイベントは呼び出されません。代わりに :func:`on_raw_member_remove` を使用してください。" -#: ../../api.rst:780 +#: ../../api.rst:821 msgid "The member who left." msgstr "脱退したメンバー。" -#: ../../api.rst:787 +#: ../../api.rst:828 msgid "Unlike :func:`on_member_remove` this is called regardless of the guild or member being in the internal cache." msgstr ":func:`on_member_remove` とは異なり、ギルドやメンバーが内部キャッシュに存在するかどうかに関係なく呼び出されます。" -#: ../../api.rst:799 +#: ../../api.rst:840 msgid "Called when a :class:`Member` updates their profile." msgstr ":class:`Member` のプロフィールが更新されたときに呼び出されます。" -#: ../../api.rst:801 -#: ../../api.rst:823 +#: ../../api.rst:842 #: ../../api.rst:864 +#: ../../api.rst:905 msgid "This is called when one or more of the following things change:" msgstr "これらのうちひとつ以上が変更されたとき呼び出されます:" -#: ../../api.rst:803 +#: ../../api.rst:844 msgid "nickname" msgstr "ニックネーム" -#: ../../api.rst:804 +#: ../../api.rst:845 #: ../../../discord/member.py:docstring of discord.member.Member.edit:16 msgid "roles" msgstr "roles" -#: ../../api.rst:805 +#: ../../api.rst:846 msgid "pending" msgstr "ペンディング状態" -#: ../../api.rst:806 +#: ../../api.rst:847 msgid "timeout" msgstr "タイムアウト" -#: ../../api.rst:807 +#: ../../api.rst:848 msgid "guild avatar" msgstr "ギルドアバター" -#: ../../api.rst:808 +#: ../../api.rst:849 msgid "flags" msgstr "flags" -#: ../../api.rst:810 +#: ../../api.rst:851 msgid "Due to a Discord limitation, this event is not dispatched when a member's timeout expires." msgstr "Discordの制限により、このイベントはメンバーのタイムアウト期間が満了した場合には発生しません。" -#: ../../api.rst:814 -#: ../../api.rst:873 +#: ../../api.rst:855 +#: ../../api.rst:914 msgid "The updated member's old info." msgstr "更新されたメンバーの更新前情報。" -#: ../../api.rst:816 -#: ../../api.rst:875 +#: ../../api.rst:857 +#: ../../api.rst:916 msgid "The updated member's updated info." msgstr "更新されたメンバーの更新後情報。" -#: ../../api.rst:821 +#: ../../api.rst:862 msgid "Called when a :class:`User` updates their profile." msgstr ":class:`User` がプロフィールを編集したとき呼び出されます。" -#: ../../api.rst:825 +#: ../../api.rst:866 msgid "avatar" msgstr "アバター" -#: ../../api.rst:826 +#: ../../api.rst:867 msgid "username" msgstr "ユーザー名" -#: ../../api.rst:827 +#: ../../api.rst:868 msgid "discriminator" msgstr "タグ" -#: ../../api.rst:831 +#: ../../api.rst:872 msgid "The updated user's old info." msgstr "更新されたユーザーの更新前情報。" -#: ../../api.rst:833 +#: ../../api.rst:874 msgid "The updated user's updated info." msgstr "更新されたユーザーの更新後情報。" -#: ../../api.rst:838 -msgid "Called when user gets banned from a :class:`Guild`." -msgstr "ユーザーが :class:`Guild` からBANされたとき呼び出されます。" +#: ../../api.rst:879 +msgid "Called when a user gets banned from a :class:`Guild`." +msgstr "" -#: ../../api.rst:842 +#: ../../api.rst:883 msgid "The guild the user got banned from." msgstr "ユーザーがBANされたギルド。" -#: ../../api.rst:844 +#: ../../api.rst:885 msgid "The user that got banned. Can be either :class:`User` or :class:`Member` depending if the user was in the guild or not at the time of removal." msgstr "BANされたユーザー。BAN時にユーザーがギルドにいたかによって、 :class:`User` か :class:`Member` になります。" -#: ../../api.rst:851 +#: ../../api.rst:892 msgid "Called when a :class:`User` gets unbanned from a :class:`Guild`." msgstr ":class:`User` が :class:`Guild` のBANを解除されたとき呼び出されます。" -#: ../../api.rst:855 +#: ../../api.rst:896 msgid "The guild the user got unbanned from." msgstr "ユーザーのBANが解除されたギルド。" -#: ../../api.rst:857 +#: ../../api.rst:898 msgid "The user that got unbanned." msgstr "Banが解除されたユーザー。" -#: ../../api.rst:862 +#: ../../api.rst:903 msgid "Called when a :class:`Member` updates their presence." msgstr ":class:`Member` がプレゼンスを変更したとき呼び出されます。" -#: ../../api.rst:866 +#: ../../api.rst:907 msgid "status" msgstr "ステータス" -#: ../../api.rst:867 +#: ../../api.rst:908 msgid "activity" msgstr "アクティビティ" -#: ../../api.rst:869 +#: ../../api.rst:910 msgid "This requires :attr:`Intents.presences` and :attr:`Intents.members` to be enabled." msgstr "これを使用するには :attr:`Intents.presences` と :attr:`Intents.members` を有効にしないといけません。" -#: ../../api.rst:879 +#: ../../api.rst:920 msgid "Messages" msgstr "Messages" -#: ../../api.rst:883 +#: ../../api.rst:924 msgid "Called when a :class:`Message` is created and sent." msgstr ":class:`Message` が作成され送信されたときに呼び出されます。" -#: ../../api.rst:889 +#: ../../api.rst:930 msgid "Your bot's own messages and private messages are sent through this event. This can lead cases of 'recursion' depending on how your bot was programmed. If you want the bot to not reply to itself, consider checking the user IDs. Note that :class:`~ext.commands.Bot` does not have this problem." msgstr "Botのメッセージとプライベートメッセージはこのイベントを通して送信されます。Botのプログラムによっては「再帰呼び出し」を続けることになります。Botが自分自身に返信しないようにするためにはユーザーIDを確認する方法が考えられます。 :class:`~ext.commands.Bot` にはこの問題は存在しません。" -#: ../../api.rst:895 +#: ../../api.rst:936 msgid "The current message." msgstr "現在のメッセージ。" -#: ../../api.rst:900 +#: ../../api.rst:941 msgid "Called when a :class:`Message` receives an update event. If the message is not found in the internal message cache, then these events will not be called. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr ":class:`Message` が更新イベントを受け取ったときに呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:905 +#: ../../api.rst:946 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_message_edit` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_message_edit` イベントを使用してください。" -#: ../../api.rst:908 +#: ../../api.rst:949 msgid "The following non-exhaustive cases trigger this event:" msgstr "以下の非網羅的ケースがこのイベントを発生させます:" -#: ../../api.rst:910 +#: ../../api.rst:951 msgid "A message has been pinned or unpinned." msgstr "メッセージをピン留め、または解除した。" -#: ../../api.rst:911 +#: ../../api.rst:952 msgid "The message content has been changed." msgstr "メッセージの内容を変更した。" -#: ../../api.rst:912 +#: ../../api.rst:953 msgid "The message has received an embed." msgstr "メッセージが埋め込みを受け取った。" -#: ../../api.rst:914 +#: ../../api.rst:955 msgid "For performance reasons, the embed server does not do this in a \"consistent\" manner." msgstr "パフォーマンス上の理由から、埋め込みのサーバーはこれを「一貫した」方法では行いません。" -#: ../../api.rst:916 +#: ../../api.rst:957 msgid "The message's embeds were suppressed or unsuppressed." msgstr "メッセージの埋め込みが削除されたり、復元されたりした。" -#: ../../api.rst:917 +#: ../../api.rst:958 msgid "A call message has received an update to its participants or ending time." msgstr "通話呼び出しメッセージの参加者や終了時刻が変わった。" -#: ../../api.rst:921 +#: ../../api.rst:962 msgid "The previous version of the message." msgstr "更新前のメッセージ。" -#: ../../api.rst:923 +#: ../../api.rst:964 msgid "The current version of the message." msgstr "更新後のメッセージ。" -#: ../../api.rst:928 +#: ../../api.rst:969 msgid "Called when a message is deleted. If the message is not found in the internal message cache, then this event will not be called. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr "メッセージが削除された際に呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:933 +#: ../../api.rst:974 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_message_delete` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_message_delete` イベントを使用してください。" -#: ../../api.rst:938 +#: ../../api.rst:979 msgid "The deleted message." msgstr "削除されたメッセージ。" -#: ../../api.rst:943 +#: ../../api.rst:984 msgid "Called when messages are bulk deleted. If none of the messages deleted are found in the internal message cache, then this event will not be called. If individual messages were not found in the internal message cache, this event will still be called, but the messages not found will not be included in the messages list. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr "メッセージが一括削除されたときに呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。個々のメッセージが見つからない場合でも、このイベントは呼び出されますが、見つからなかったメッセージはメッセージのリストに含まれません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:950 +#: ../../api.rst:991 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_bulk_message_delete` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_bulk_message_delete` イベントを使用してください。" -#: ../../api.rst:955 +#: ../../api.rst:996 msgid "The messages that have been deleted." msgstr "削除されたメッセージのリスト。" -#: ../../api.rst:960 +#: ../../api.rst:1001 msgid "Called when a message is edited. Unlike :func:`on_message_edit`, this is called regardless of the state of the internal message cache." msgstr "メッセージが編集されたときに呼び出されます。 :func:`on_message_edit` とは異なり、これは内部のメッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:963 +#: ../../api.rst:1004 msgid "If the message is found in the message cache, it can be accessed via :attr:`RawMessageUpdateEvent.cached_message`. The cached message represents the message before it has been edited. For example, if the content of a message is modified and triggers the :func:`on_raw_message_edit` coroutine, the :attr:`RawMessageUpdateEvent.cached_message` will return a :class:`Message` object that represents the message before the content was modified." msgstr "メッセージがメッセージキャッシュに存在した場合、 :attr:`RawMessageUpdateEvent.cached_message` を介してそのメッセージにアクセスすることができます。キャッシュされていたメッセージは編集前のメッセージです。たとえば、メッセージの内容が編集され、 :func:`on_raw_message_edit` が発火された場合、 :attr:`RawMessageUpdateEvent.cached_message` は内容が編集される前の情報を持つ :class:`Message` オブジェクトを返します。" -#: ../../api.rst:969 +#: ../../api.rst:1010 msgid "Due to the inherently raw nature of this event, the data parameter coincides with the raw data given by the :ddocs:`gateway `." msgstr "このイベントの性質は、本質的に生表現のため、データのパラメータは :ddocs:`ゲートウェイ ` によって与えられた生データと一致します。" -#: ../../api.rst:972 +#: ../../api.rst:1013 msgid "Since the data payload can be partial, care must be taken when accessing stuff in the dictionary. One example of a common case of partial data is when the ``'content'`` key is inaccessible. This denotes an \"embed\" only edit, which is an edit in which only the embeds are updated by the Discord embed server." msgstr "データのペイロードが部分的であるため、データにアクセスするときは気をつけてください。部分的なデータの主な場合のひとつは、``'content'`` にアクセスできない場合です。Discordの埋め込みサーバーによって、埋め込みが更新される、\"埋め込み\"しか変わっていない編集がそうです。" -#: ../../api.rst:985 +#: ../../api.rst:1026 msgid "Called when a message is deleted. Unlike :func:`on_message_delete`, this is called regardless of the message being in the internal message cache or not." msgstr "メッセージが削除されたときに呼び出されます。 :func:`on_message_delete` とは異なり、削除されたメッセージが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:988 +#: ../../api.rst:1029 msgid "If the message is found in the message cache, it can be accessed via :attr:`RawMessageDeleteEvent.cached_message`" msgstr "メッセージがメッセージキャッシュ内に見つかった場合、 :attr:`RawMessageDeleteEvent.cached_message` を介してアクセスすることができます。" -#: ../../api.rst:998 +#: ../../api.rst:1039 msgid "Called when a bulk delete is triggered. Unlike :func:`on_bulk_message_delete`, this is called regardless of the messages being in the internal message cache or not." msgstr "メッセージが一括削除されたときに呼び出されます。 :func:`on_bulk_message_delete` とは異なり、削除されたメッセージが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1001 +#: ../../api.rst:1042 msgid "If the messages are found in the message cache, they can be accessed via :attr:`RawBulkMessageDeleteEvent.cached_messages`" msgstr "メッセージがメッセージキャッシュ内に見つかった場合、 :attr:`RawBulkMessageDeleteEvent.cached_messages` を介してアクセスすることができます。" -#: ../../api.rst:1010 +#: ../../api.rst:1051 msgid "Reactions" msgstr "Reactions" -#: ../../api.rst:1014 +#: ../../api.rst:1055 msgid "Called when a message has a reaction added to it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_add` instead." msgstr "メッセージにリアクションが追加されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_add` の利用を検討してください。" -#: ../../api.rst:1020 +#: ../../api.rst:1061 msgid "To get the :class:`Message` being reacted, access it via :attr:`Reaction.message`." msgstr "リアクションの付いた :class:`Message` を取得するには、 :attr:`Reaction.message` を使ってください。" -#: ../../api.rst:1022 -#: ../../api.rst:1065 -#: ../../api.rst:1078 -#: ../../api.rst:1091 -#: ../../api.rst:1101 +#: ../../api.rst:1063 +#: ../../api.rst:1118 +#: ../../api.rst:1131 +#: ../../api.rst:1144 +#: ../../api.rst:1154 msgid "This requires :attr:`Intents.reactions` to be enabled." msgstr ":attr:`Intents.reactions` を有効にする必要があります。" -#: ../../api.rst:1026 +#: ../../api.rst:1067 msgid "This doesn't require :attr:`Intents.members` within a guild context, but due to Discord not providing updated user information in a direct message it's required for direct messages to receive this event. Consider using :func:`on_raw_reaction_add` if you need this and do not otherwise want to enable the members intent." msgstr "ギルド内では :attr:`Intents.members` は有効にしなくてもよいですが、DiscordがDM内では更新されたユーザーの情報を提供しないため、DMではこのインテントが必要です。 このイベントが必要でメンバーインテントを有効化したくない場合は :func:`on_raw_reaction_add` の使用を検討してください。" -#: ../../api.rst:1032 -#: ../../api.rst:1054 +#: ../../api.rst:1075 +msgid "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." +msgstr "" + +#: ../../api.rst:1079 +#: ../../api.rst:1107 msgid "The current state of the reaction." msgstr "リアクションの現在の状態。" -#: ../../api.rst:1034 +#: ../../api.rst:1081 msgid "The user who added the reaction." msgstr "リアクションを追加したユーザー。" -#: ../../api.rst:1039 +#: ../../api.rst:1086 msgid "Called when a message has a reaction removed from it. Similar to on_message_edit, if the message is not found in the internal message cache, then this event will not be called." msgstr "メッセージのリアクションが取り除かれたときに呼び出されます。on_message_editのように、内部のメッセージキャッシュにメッセージがないときには、このイベントは呼び出されません。" -#: ../../api.rst:1045 +#: ../../api.rst:1092 msgid "To get the message being reacted, access it via :attr:`Reaction.message`." msgstr "リアクションの付いたメッセージを取得するには、 :attr:`Reaction.message` を使ってください。" -#: ../../api.rst:1047 +#: ../../api.rst:1094 msgid "This requires both :attr:`Intents.reactions` and :attr:`Intents.members` to be enabled." msgstr "これを使用するには :attr:`Intents.reactions` と :attr:`Intents.members` の両方を有効にしないといけません。" -#: ../../api.rst:1051 +#: ../../api.rst:1098 msgid "Consider using :func:`on_raw_reaction_remove` if you need this and do not want to enable the members intent." msgstr "このイベントが必要でメンバーインテントを有効化したくない場合は :func:`on_raw_reaction_remove` の使用を検討してください。" -#: ../../api.rst:1056 +#: ../../api.rst:1103 +msgid "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." +msgstr "" + +#: ../../api.rst:1109 msgid "The user whose reaction was removed." msgstr "リアクションが除去されたユーザー。" -#: ../../api.rst:1061 +#: ../../api.rst:1114 msgid "Called when a message has all its reactions removed from it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_clear` instead." msgstr "メッセージからすべてのリアクションが削除されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_clear` の利用を検討してください。" -#: ../../api.rst:1067 +#: ../../api.rst:1120 msgid "The message that had its reactions cleared." msgstr "リアクションが削除されたメッセージ。" -#: ../../api.rst:1069 +#: ../../api.rst:1122 msgid "The reactions that were removed." msgstr "除去されたリアクション。" -#: ../../api.rst:1074 +#: ../../api.rst:1127 msgid "Called when a message has a specific reaction removed from it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_clear_emoji` instead." msgstr "メッセージから特定の絵文字のリアクションが除去されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_clear_emoji` の利用を検討してください。" -#: ../../api.rst:1082 +#: ../../api.rst:1135 msgid "The reaction that got cleared." msgstr "除去されたリアクション。" -#: ../../api.rst:1088 +#: ../../api.rst:1141 msgid "Called when a message has a reaction added. Unlike :func:`on_reaction_add`, this is called regardless of the state of the internal message cache." msgstr "メッセージにリアクションが追加されたときに呼び出されます。 :func:`on_reaction_add` とは異なり、これは内部のメッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1098 +#: ../../api.rst:1151 msgid "Called when a message has a reaction removed. Unlike :func:`on_reaction_remove`, this is called regardless of the state of the internal message cache." msgstr "メッセージからリアクションが削除されたときに呼び出されます。 :func:`on_reaction_remove` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1108 +#: ../../api.rst:1161 msgid "Called when a message has all its reactions removed. Unlike :func:`on_reaction_clear`, this is called regardless of the state of the internal message cache." msgstr "メッセージからリアクションがすべて削除されたときに呼び出されます。 :func:`on_reaction_clear` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1118 +#: ../../api.rst:1171 msgid "Called when a message has a specific reaction removed from it. Unlike :func:`on_reaction_clear_emoji` this is called regardless of the state of the internal message cache." msgstr "メッセージから特定の絵文字のリアクションがすべて除去されたときに呼び出されます。 :func:`on_reaction_clear_emoji` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1130 +#: ../../api.rst:1183 msgid "Roles" msgstr "Roles" -#: ../../api.rst:1135 +#: ../../api.rst:1188 msgid "Called when a :class:`Guild` creates or deletes a new :class:`Role`." msgstr ":class:`Guild` で :class:`Role` が新しく作成されたか、削除されたときに呼び出されます。" -#: ../../api.rst:1137 +#: ../../api.rst:1190 msgid "To get the guild it belongs to, use :attr:`Role.guild`." msgstr "ギルドを取得するには :attr:`Role.guild` を使用してください。" -#: ../../api.rst:1141 +#: ../../api.rst:1194 msgid "The role that was created or deleted." msgstr "作成、または削除されたロール。" -#: ../../api.rst:1146 +#: ../../api.rst:1199 msgid "Called when a :class:`Role` is changed guild-wide." msgstr ":class:`Role` がギルド全体で変更されたときに呼び出されます。" -#: ../../api.rst:1150 +#: ../../api.rst:1203 msgid "The updated role's old info." msgstr "更新されたロールの更新前情報。" -#: ../../api.rst:1152 +#: ../../api.rst:1205 msgid "The updated role's updated info." msgstr "更新されたロールの更新後情報。" -#: ../../api.rst:1157 +#: ../../api.rst:1210 msgid "Scheduled Events" msgstr "Scheduled Events" -#: ../../api.rst:1162 +#: ../../api.rst:1215 msgid "Called when a :class:`ScheduledEvent` is created or deleted." msgstr ":class:`ScheduledEvent` が作成または削除されたときに呼び出されます。" -#: ../../api.rst:1164 -#: ../../api.rst:1175 -#: ../../api.rst:1197 +#: ../../api.rst:1217 +#: ../../api.rst:1228 +#: ../../api.rst:1250 msgid "This requires :attr:`Intents.guild_scheduled_events` to be enabled." msgstr ":attr:`Intents.guild_scheduled_events` を有効にする必要があります。" -#: ../../api.rst:1168 +#: ../../api.rst:1221 msgid "The scheduled event that was created or deleted." msgstr "作成、または削除されたスケジュールイベント。" -#: ../../api.rst:1173 +#: ../../api.rst:1226 msgid "Called when a :class:`ScheduledEvent` is updated." msgstr ":class:`ScheduledEvent` が変更されたときに呼び出されます。" -#: ../../api.rst:1177 -#: ../../api.rst:1224 -#: ../../api.rst:1376 +#: ../../api.rst:1230 +#: ../../api.rst:1277 +#: ../../api.rst:1429 msgid "The following, but not limited to, examples illustrate when this event is called:" msgstr "これらの場合に限りませんが、例を挙げると、以下の場合に呼び出されます:" -#: ../../api.rst:1179 +#: ../../api.rst:1232 msgid "The scheduled start/end times are changed." msgstr "スケジュールされた開始・終了時刻が変更された。" -#: ../../api.rst:1180 +#: ../../api.rst:1233 msgid "The channel is changed." msgstr "チャンネルが変更された時。" -#: ../../api.rst:1181 +#: ../../api.rst:1234 msgid "The description is changed." msgstr "説明が変更された時。" -#: ../../api.rst:1182 +#: ../../api.rst:1235 msgid "The status is changed." msgstr "ステータスが変更された時。" -#: ../../api.rst:1183 +#: ../../api.rst:1236 msgid "The image is changed." msgstr "画像が変更された時。" -#: ../../api.rst:1187 +#: ../../api.rst:1240 msgid "The scheduled event before the update." msgstr "変更前のスケジュールイベント。" -#: ../../api.rst:1189 +#: ../../api.rst:1242 msgid "The scheduled event after the update." msgstr "変更後のスケジュールイベント。" -#: ../../api.rst:1195 +#: ../../api.rst:1248 msgid "Called when a user is added or removed from a :class:`ScheduledEvent`." msgstr ":class:`ScheduledEvent` からユーザーが追加または削除されたときに呼び出されます。" -#: ../../api.rst:1201 +#: ../../api.rst:1254 msgid "The scheduled event that the user was added or removed from." msgstr "ユーザーが追加または削除されたスケジュールイベント。" -#: ../../api.rst:1203 +#: ../../api.rst:1256 msgid "The user that was added or removed." msgstr "追加・削除されたユーザー。" -#: ../../api.rst:1208 +#: ../../api.rst:1261 msgid "Stages" msgstr "Stages" -#: ../../api.rst:1213 +#: ../../api.rst:1266 msgid "Called when a :class:`StageInstance` is created or deleted for a :class:`StageChannel`." msgstr ":class:`StageChannel` の :class:`StageInstance` が作成または削除されたときに呼び出されます。" -#: ../../api.rst:1217 +#: ../../api.rst:1270 msgid "The stage instance that was created or deleted." msgstr "作成、または削除されたステージインスタンス。" -#: ../../api.rst:1222 +#: ../../api.rst:1275 msgid "Called when a :class:`StageInstance` is updated." msgstr ":class:`StageInstance` が変更されたときに呼び出されます。" -#: ../../api.rst:1226 +#: ../../api.rst:1279 msgid "The topic is changed." msgstr "トピックが変更された時。" -#: ../../api.rst:1227 +#: ../../api.rst:1280 msgid "The privacy level is changed." msgstr "プライバシーレベルが変更された時。" -#: ../../api.rst:1231 +#: ../../api.rst:1284 msgid "The stage instance before the update." msgstr "更新前のステージインスタンス。" -#: ../../api.rst:1233 +#: ../../api.rst:1286 msgid "The stage instance after the update." msgstr "更新後のステージインスタンス。" -#: ../../api.rst:1237 +#: ../../api.rst:1290 msgid "Threads" msgstr "Threads" -#: ../../api.rst:1241 +#: ../../api.rst:1294 msgid "Called whenever a thread is created." msgstr "スレッドが作成されたときに発生します。" -#: ../../api.rst:1243 -#: ../../api.rst:1256 -#: ../../api.rst:1286 -#: ../../api.rst:1310 +#: ../../api.rst:1296 +#: ../../api.rst:1309 +#: ../../api.rst:1339 +#: ../../api.rst:1363 msgid "Note that you can get the guild from :attr:`Thread.guild`." msgstr "ギルドは :attr:`Thread.guild` から取得できます。" -#: ../../api.rst:1249 +#: ../../api.rst:1302 msgid "The thread that was created." msgstr "作成されたスレッド。" -#: ../../api.rst:1254 +#: ../../api.rst:1307 msgid "Called whenever a thread is joined." msgstr "スレッドに参加したときに呼び出されます。" -#: ../../api.rst:1262 +#: ../../api.rst:1315 msgid "The thread that got joined." msgstr "参加したスレッド。" -#: ../../api.rst:1267 +#: ../../api.rst:1320 msgid "Called whenever a thread is updated. If the thread could not be found in the internal cache this event will not be called. Threads will not be in the cache if they are archived." msgstr "スレッドが更新されたときに呼び出されます。スレッドが内部キャッシュに見つからなかった場合、このイベントは呼び出されません。 スレッドがアーカイブされている場合、キャッシュには含まれません。" -#: ../../api.rst:1271 +#: ../../api.rst:1324 msgid "If you need this information use :func:`on_raw_thread_update` instead." msgstr "この情報が必要な場合は、代わりに :func:`on_raw_thread_update` を使用してください。" -#: ../../api.rst:1277 +#: ../../api.rst:1330 msgid "The updated thread's old info." msgstr "古いスレッドの情報。" -#: ../../api.rst:1279 +#: ../../api.rst:1332 msgid "The updated thread's new info." msgstr "新しいスレッドの情報。" -#: ../../api.rst:1284 +#: ../../api.rst:1337 msgid "Called whenever a thread is removed. This is different from a thread being deleted." msgstr "スレッドが除去されたときに呼び出されます。これはスレッドの削除とは異なります。" -#: ../../api.rst:1292 +#: ../../api.rst:1345 msgid "Due to technical limitations, this event might not be called as soon as one expects. Since the library tracks thread membership locally, the API only sends updated thread membership status upon being synced by joining a thread." msgstr "技術的な制約のためこのイベントは期待通りの早さで呼び出されない場合があります。ライブラリーがスレッドの参加をローカルで追跡するため、APIは更新されたスレッドの参加状態をスレッドの参加時にのみ同期します。" -#: ../../api.rst:1299 +#: ../../api.rst:1352 msgid "The thread that got removed." msgstr "削除されたスレッド。" -#: ../../api.rst:1304 +#: ../../api.rst:1357 msgid "Called whenever a thread is deleted. If the thread could not be found in the internal cache this event will not be called. Threads will not be in the cache if they are archived." msgstr "スレッドが削除されたときに呼び出されます。スレッドが内部キャッシュに見つからなかった場合、このイベントは呼び出されません。 スレッドがアーカイブされている場合、キャッシュには含まれません。" -#: ../../api.rst:1308 +#: ../../api.rst:1361 msgid "If you need this information use :func:`on_raw_thread_delete` instead." msgstr "この情報が必要な場合は、代わりに :func:`on_raw_thread_delete` を使用してください。" -#: ../../api.rst:1316 +#: ../../api.rst:1369 msgid "The thread that got deleted." msgstr "削除されたスレッド。" -#: ../../api.rst:1321 +#: ../../api.rst:1374 msgid "Called whenever a thread is updated. Unlike :func:`on_thread_update` this is called regardless of the thread being in the internal thread cache or not." msgstr "スレッドが更新されたときに呼び出されます。 :func:`on_thread_update` とは異なり、更新されたスレッドが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1333 +#: ../../api.rst:1386 msgid "Called whenever a thread is deleted. Unlike :func:`on_thread_delete` this is called regardless of the thread being in the internal thread cache or not." msgstr "スレッドが削除されたときに呼び出されます。 :func:`on_thread_delete` とは異なり、削除されたスレッドが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1346 +#: ../../api.rst:1399 msgid "Called when a :class:`ThreadMember` leaves or joins a :class:`Thread`." msgstr ":class:`ThreadMember` が :class:`Thread` に参加したり退出したりしたときに呼び出されます。" -#: ../../api.rst:1348 +#: ../../api.rst:1401 msgid "You can get the thread a member belongs in by accessing :attr:`ThreadMember.thread`." msgstr "メンバーが所属するスレッドは :attr:`ThreadMember.thread` から取得できます。" -#: ../../api.rst:1354 +#: ../../api.rst:1407 msgid "The member who joined or left." msgstr "参加、または脱退したメンバー。" -#: ../../api.rst:1359 +#: ../../api.rst:1412 msgid "Called when a :class:`ThreadMember` leaves a :class:`Thread`. Unlike :func:`on_thread_member_remove` this is called regardless of the member being in the internal thread's members cache or not." msgstr ":class:`ThreadMember` が :class:`Thread` を退出したときに呼び出されます。 :func:`on_thread_member_remove` とは異なり、メンバーが内部スレッドメンバーキャッシュに存在するかどうかに関係なく呼び出されます。" -#: ../../api.rst:1370 +#: ../../api.rst:1423 msgid "Voice" msgstr "Voice" -#: ../../api.rst:1374 +#: ../../api.rst:1427 msgid "Called when a :class:`Member` changes their :class:`VoiceState`." msgstr ":class:`Member` が :class:`VoiceState` を変更したとき呼び出されます。" -#: ../../api.rst:1378 +#: ../../api.rst:1431 msgid "A member joins a voice or stage channel." msgstr "メンバーがボイスチャンネルやステージチャンネルに参加したとき。" -#: ../../api.rst:1379 +#: ../../api.rst:1432 msgid "A member leaves a voice or stage channel." msgstr "メンバーがボイスチャンネルやステージチャンネルから退出したとき。" -#: ../../api.rst:1380 +#: ../../api.rst:1433 msgid "A member is muted or deafened by their own accord." msgstr "メンバーが自身でマイクやスピーカーをミュートしたとき。" -#: ../../api.rst:1381 +#: ../../api.rst:1434 msgid "A member is muted or deafened by a guild administrator." msgstr "メンバーがギルドの管理者によってマイクやスピーカーをミュートされたとき。" -#: ../../api.rst:1383 +#: ../../api.rst:1436 msgid "This requires :attr:`Intents.voice_states` to be enabled." msgstr ":attr:`Intents.voice_states` を有効にする必要があります。" -#: ../../api.rst:1385 +#: ../../api.rst:1438 msgid "The member whose voice states changed." msgstr "ボイスの状態が変わった `Member` 。" -#: ../../api.rst:1387 +#: ../../api.rst:1440 msgid "The voice state prior to the changes." msgstr "更新前のボイス状態。" -#: ../../api.rst:1389 +#: ../../api.rst:1442 msgid "The voice state after the changes." msgstr "更新後のボイス状態。" -#: ../../api.rst:1395 +#: ../../api.rst:1448 msgid "Utility Functions" msgstr "ユーティリティ関数" @@ -4152,15 +4508,15 @@ msgstr "メンションをエスケープするテキスト。" msgid "The text with the mentions removed." msgstr "メンションが削除されたテキスト。" -#: ../../api.rst:1419 +#: ../../api.rst:1472 msgid "A data class which represents a resolved invite returned from :func:`discord.utils.resolve_invite`." msgstr ":func:`discord.utils.resolve_invite` から返された解決済みの招待を表すデータクラス。" -#: ../../api.rst:1423 +#: ../../api.rst:1476 msgid "The invite code." msgstr "招待コード。" -#: ../../api.rst:1429 +#: ../../api.rst:1482 msgid "The id of the scheduled event that the invite refers to." msgstr "招待が参照するスケジュールイベントのID。" @@ -4242,8 +4598,8 @@ msgid "Example Output" msgstr "出力例" #: ../../../discord/utils.py:docstring of discord.utils.format_dt:6 -#: ../../api.rst:3370 -#: ../../api.rst:3390 +#: ../../api.rst:3579 +#: ../../api.rst:3599 msgid "Description" msgstr "説明" @@ -4371,1934 +4727,2049 @@ msgstr "指定されたサイズのチャンクを生成する新しいイテレ msgid "Union[:class:`Iterator`, :class:`AsyncIterator`]" msgstr "Union[:class:`Iterator`, :class:`AsyncIterator`]" -#: ../../api.rst:1448 +#: ../../api.rst:1501 msgid "A type safe sentinel used in the library to represent something as missing. Used to distinguish from ``None`` values." msgstr "ライブラリで見つからないものを表現するために使用されるタイプセーフなセンチネル型。 ``None`` 値と区別するために使用されます。" -#: ../../api.rst:1455 +#: ../../api.rst:1508 msgid "Enumerations" msgstr "列挙型" -#: ../../api.rst:1457 +#: ../../api.rst:1510 msgid "The API provides some enumerations for certain types of strings to avoid the API from being stringly typed in case the strings change in the future." msgstr "APIは、文字列が将来変わることに備え、文字列を直書きするのを防ぐために、いくらかの文字列の列挙型を提供します。" -#: ../../api.rst:1460 +#: ../../api.rst:1513 msgid "All enumerations are subclasses of an internal class which mimics the behaviour of :class:`enum.Enum`." msgstr "列挙型はすべて :class:`enum.Enum` の動作を模倣した内部クラスのサブクラスです。" -#: ../../api.rst:1465 +#: ../../api.rst:1518 msgid "Specifies the type of channel." msgstr "特定チャンネルのチャンネルタイプ。" -#: ../../api.rst:1469 +#: ../../api.rst:1522 msgid "A text channel." msgstr "テキストチャンネル。" -#: ../../api.rst:1472 +#: ../../api.rst:1525 msgid "A voice channel." msgstr "ボイスチャンネル。" -#: ../../api.rst:1475 +#: ../../api.rst:1528 msgid "A private text channel. Also called a direct message." msgstr "プライベートのテキストチャンネル。ダイレクトメッセージとも呼ばれています。" -#: ../../api.rst:1478 +#: ../../api.rst:1531 msgid "A private group text channel." msgstr "プライベートのグループDM。" -#: ../../api.rst:1481 +#: ../../api.rst:1534 msgid "A category channel." msgstr "カテゴリチャンネル。" -#: ../../api.rst:1484 +#: ../../api.rst:1537 msgid "A guild news channel." msgstr "ギルドのニュースチャンネル。" -#: ../../api.rst:1488 +#: ../../api.rst:1541 msgid "A guild stage voice channel." msgstr "ギルドのステージボイスチャンネル。" -#: ../../api.rst:1494 +#: ../../api.rst:1547 msgid "A news thread" msgstr "ニューススレッド。" -#: ../../api.rst:1500 +#: ../../api.rst:1553 msgid "A public thread" msgstr "パブリックスレッド。" -#: ../../api.rst:1506 +#: ../../api.rst:1559 msgid "A private thread" msgstr "プライベートスレッド。" -#: ../../api.rst:1512 +#: ../../api.rst:1565 msgid "A forum channel." msgstr "フォーラムチャンネル。" -#: ../../api.rst:1518 +#: ../../api.rst:1571 +msgid "A media channel." +msgstr "" + +#: ../../api.rst:1577 msgid "Specifies the type of :class:`Message`. This is used to denote if a message is to be interpreted as a system message or a regular message." msgstr ":class:`Message` のタイプを指定します。これは、メッセージが通常のものかシステムメッセージかを判断するのに使用できます。" -#: ../../api.rst:1525 +#: ../../api.rst:1584 #: ../../../discord/message.py:docstring of discord.message.Message:7 msgid "Checks if two messages are equal." msgstr "二つのメッセージが等しいかを比較します。" -#: ../../api.rst:1528 +#: ../../api.rst:1587 #: ../../../discord/message.py:docstring of discord.message.Message:11 msgid "Checks if two messages are not equal." msgstr "二つのメッセージが等しくないかを比較します。" -#: ../../api.rst:1532 +#: ../../api.rst:1591 msgid "The default message type. This is the same as regular messages." msgstr "デフォルトのメッセージ。これは通常のメッセージと同じです。" -#: ../../api.rst:1535 +#: ../../api.rst:1594 msgid "The system message when a user is added to a group private message or a thread." msgstr "ユーザーがグループプライベートメッセージまたはスレッドに追加されたときのシステムメッセージ。" -#: ../../api.rst:1539 +#: ../../api.rst:1598 msgid "The system message when a user is removed from a group private message or a thread." msgstr "ユーザーがグループプライベートメッセージまたはスレッドから削除されたときのシステムメッセージ。" -#: ../../api.rst:1543 +#: ../../api.rst:1602 msgid "The system message denoting call state, e.g. missed call, started call, etc." msgstr "通話の状態を示すシステムメッセージ。例: 不在着信、通話の開始、その他。" -#: ../../api.rst:1547 +#: ../../api.rst:1606 msgid "The system message denoting that a channel's name has been changed." msgstr "チャンネル名の変更を示すシステムメッセージ。" -#: ../../api.rst:1550 +#: ../../api.rst:1609 msgid "The system message denoting that a channel's icon has been changed." msgstr "チャンネルのアイコンの変更を示すシステムメッセージ。" -#: ../../api.rst:1553 +#: ../../api.rst:1612 msgid "The system message denoting that a pinned message has been added to a channel." msgstr "ピン留めの追加を示すシステムメッセージ。" -#: ../../api.rst:1556 +#: ../../api.rst:1615 msgid "The system message denoting that a new member has joined a Guild." msgstr "ギルドの新規メンバーの参加を示すシステムメッセージ。" -#: ../../api.rst:1560 +#: ../../api.rst:1619 msgid "The system message denoting that a member has \"nitro boosted\" a guild." msgstr "メンバーがギルドを「ニトロブースト」したことを表すシステムメッセージ。" -#: ../../api.rst:1563 +#: ../../api.rst:1622 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 1." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル1に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1567 +#: ../../api.rst:1626 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 2." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル2に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1571 +#: ../../api.rst:1630 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 3." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル3に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1575 +#: ../../api.rst:1634 msgid "The system message denoting that an announcement channel has been followed." msgstr "アナウンスチャンネルがフォローされたことを表すシステムメッセージ。" -#: ../../api.rst:1580 +#: ../../api.rst:1639 msgid "The system message denoting that a member is streaming in the guild." msgstr "メンバーがギルドでストリーミングしていることを表すシステムメッセージ。" -#: ../../api.rst:1585 +#: ../../api.rst:1644 msgid "The system message denoting that the guild is no longer eligible for Server Discovery." msgstr "ギルドがサーバー発見の資格を持たなくなったことを示すシステムメッセージ。" -#: ../../api.rst:1591 +#: ../../api.rst:1650 msgid "The system message denoting that the guild has become eligible again for Server Discovery." msgstr "ギルドがサーバー発見の資格を再び持ったことを示すシステムメッセージ。" -#: ../../api.rst:1597 +#: ../../api.rst:1656 msgid "The system message denoting that the guild has failed to meet the Server Discovery requirements for one week." msgstr "ギルドが1週間サーバー発見の要件を満たすことに失敗したことを示すシステムメッセージ。" -#: ../../api.rst:1603 +#: ../../api.rst:1662 msgid "The system message denoting that the guild has failed to meet the Server Discovery requirements for 3 weeks in a row." msgstr "ギルドが連続で3週間サーバー発見の要件を満たすことに失敗したというシステムメッセージ。" -#: ../../api.rst:1609 +#: ../../api.rst:1668 msgid "The system message denoting that a thread has been created. This is only sent if the thread has been created from an older message. The period of time required for a message to be considered old cannot be relied upon and is up to Discord." msgstr "スレッドが作成されたことを示すシステムメッセージ。これはスレッドが古いメッセージから作成されたときにのみ送信されます。メッセージが古いものとされる時間の閾値は信頼できずDiscord次第です。" -#: ../../api.rst:1617 +#: ../../api.rst:1676 msgid "The system message denoting that the author is replying to a message." msgstr "送信者がメッセージに返信したことを示すシステムメッセージ。" -#: ../../api.rst:1622 +#: ../../api.rst:1681 msgid "The system message denoting that a slash command was executed." msgstr "スラッシュコマンドを実行したことを示すシステムメッセージ。" -#: ../../api.rst:1627 +#: ../../api.rst:1686 msgid "The system message sent as a reminder to invite people to the guild." msgstr "人々をギルドに招待することのリマインダーとして送信されたシステムメッセージ。" -#: ../../api.rst:1632 +#: ../../api.rst:1691 msgid "The system message denoting the message in the thread that is the one that started the thread's conversation topic." msgstr "スレッドの会話を始めたメッセージであるスレッド内のメッセージを示すシステムメッセージ。" -#: ../../api.rst:1638 +#: ../../api.rst:1697 msgid "The system message denoting that a context menu command was executed." msgstr "コンテキストメニューコマンドを実行したことを示すシステムメッセージ。" -#: ../../api.rst:1643 +#: ../../api.rst:1702 msgid "The system message sent when an AutoMod rule is triggered. This is only sent if the rule is configured to sent an alert when triggered." msgstr "自動管理ルールが発動されたときに送信されるシステムメッセージ。ルールが発動されたときにアラートを送信するように設定されている場合にのみ送信されます。" -#: ../../api.rst:1649 +#: ../../api.rst:1708 msgid "The system message sent when a user purchases or renews a role subscription." msgstr "ユーザーがロールサブスクリプションを購入または更新したときに送信されるシステムメッセージ。" -#: ../../api.rst:1654 +#: ../../api.rst:1713 msgid "The system message sent when a user is given an advertisement to purchase a premium tier for an application during an interaction." msgstr "ユーザーに対し、インタラクション中にアプリケーションのプレミアム版を購入する広告を表示するときに送信されるシステムメッセージ。" -#: ../../api.rst:1660 +#: ../../api.rst:1719 msgid "The system message sent when the stage starts." msgstr "ステージ開始時に送信されるシステムメッセージ。" -#: ../../api.rst:1665 +#: ../../api.rst:1724 msgid "The system message sent when the stage ends." msgstr "ステージ終了時に送信されるシステムメッセージ。" -#: ../../api.rst:1670 +#: ../../api.rst:1729 msgid "The system message sent when the stage speaker changes." msgstr "ステージの発言者が変わるときに送信されるシステムメッセージ。" -#: ../../api.rst:1675 +#: ../../api.rst:1734 msgid "The system message sent when a user is requesting to speak by raising their hands." msgstr "ユーザーが挙手して発言許可を求めているときに送信されるシステムメッセージ。" -#: ../../api.rst:1680 +#: ../../api.rst:1739 msgid "The system message sent when the stage topic changes." msgstr "ステージのトピックが変わるときに送信されるシステムメッセージ。" -#: ../../api.rst:1685 +#: ../../api.rst:1744 msgid "The system message sent when an application's premium subscription is purchased for the guild." msgstr "アプリケーションのプレミアムサブスクリプションがギルドで購入されたときに送信されるシステムメッセージ。" -#: ../../api.rst:1691 +#: ../../api.rst:1750 +msgid "The system message sent when security actions is enabled." +msgstr "" + +#: ../../api.rst:1756 +msgid "The system message sent when security actions is disabled." +msgstr "" + +#: ../../api.rst:1762 +msgid "The system message sent when a raid is reported." +msgstr "" + +#: ../../api.rst:1768 +msgid "The system message sent when a false alarm is reported." +msgstr "" + +#: ../../api.rst:1774 msgid "Represents Discord User flags." msgstr "Discordユーザーフラグを表します。" -#: ../../api.rst:1695 +#: ../../api.rst:1778 msgid "The user is a Discord Employee." msgstr "ユーザーはDiscordの従業員です。" -#: ../../api.rst:1698 +#: ../../api.rst:1781 msgid "The user is a Discord Partner." msgstr "ユーザーはDiscordパートナーです。" -#: ../../api.rst:1701 +#: ../../api.rst:1784 msgid "The user is a HypeSquad Events member." msgstr "ユーザーはHypeSquad Eventsメンバーです。" -#: ../../api.rst:1704 +#: ../../api.rst:1787 msgid "The user is a Bug Hunter." msgstr "ユーザーはバグハンターです。" -#: ../../api.rst:1707 +#: ../../api.rst:1790 msgid "The user has SMS recovery for Multi Factor Authentication enabled." msgstr "ユーザーの多要素認証のSMSリカバリーが有効になっています。" -#: ../../api.rst:1710 +#: ../../api.rst:1793 msgid "The user has dismissed the Discord Nitro promotion." msgstr "ユーザーはDiscord Nitroプロモーションを無視しました。" -#: ../../api.rst:1713 +#: ../../api.rst:1796 msgid "The user is a HypeSquad Bravery member." msgstr "ユーザーはHypeSquad Braveryのメンバーです。" -#: ../../api.rst:1716 +#: ../../api.rst:1799 msgid "The user is a HypeSquad Brilliance member." msgstr "ユーザーはHypeSquad Brillianceのメンバーです。" -#: ../../api.rst:1719 +#: ../../api.rst:1802 msgid "The user is a HypeSquad Balance member." msgstr "ユーザーはHypeSquad Balanceのメンバーです。" -#: ../../api.rst:1722 +#: ../../api.rst:1805 msgid "The user is an Early Supporter." msgstr "ユーザーは早期サポーターです。" -#: ../../api.rst:1725 +#: ../../api.rst:1808 msgid "The user is a Team User." msgstr "ユーザーはチームユーザーです。" -#: ../../api.rst:1728 +#: ../../api.rst:1811 msgid "The user is a system user (i.e. represents Discord officially)." msgstr "ユーザーはシステムユーザーです。(つまり、Discord公式を表しています)" -#: ../../api.rst:1731 +#: ../../api.rst:1814 msgid "The user has an unread system message." msgstr "ユーザーに未読のシステムメッセージがあります。" -#: ../../api.rst:1734 +#: ../../api.rst:1817 msgid "The user is a Bug Hunter Level 2." msgstr "ユーザーはバグハンターレベル2です。" -#: ../../api.rst:1737 +#: ../../api.rst:1820 msgid "The user is a Verified Bot." msgstr "ユーザーは認証済みボットです。" -#: ../../api.rst:1740 +#: ../../api.rst:1823 msgid "The user is an Early Verified Bot Developer." msgstr "ユーザーは早期認証Botデベロッパーです。" -#: ../../api.rst:1743 +#: ../../api.rst:1826 msgid "The user is a Moderator Programs Alumni." msgstr "ユーザーはモデレータープログラム卒業生です。" -#: ../../api.rst:1746 +#: ../../api.rst:1829 msgid "The user is a bot that only uses HTTP interactions and is shown in the online member list." msgstr "ユーザーはHTTPインタラクションのみを使用し、オンラインメンバーのリストに表示されるボットです。" -#: ../../api.rst:1751 +#: ../../api.rst:1834 msgid "The user is flagged as a spammer by Discord." msgstr "ユーザーはDiscordよりスパマーとフラグ付けされました。" -#: ../../api.rst:1757 +#: ../../api.rst:1840 msgid "The user is an active developer." msgstr "ユーザーはアクティブな開発者です。" -#: ../../api.rst:1763 +#: ../../api.rst:1846 msgid "Specifies the type of :class:`Activity`. This is used to check how to interpret the activity itself." msgstr ":class:`Activity` のタイプを指定します。これはアクティビティをどう解釈するか確認するために使われます。" -#: ../../api.rst:1768 +#: ../../api.rst:1851 msgid "An unknown activity type. This should generally not happen." msgstr "不明なアクティビティタイプ。これは通常起こらないはずです。" -#: ../../api.rst:1771 +#: ../../api.rst:1854 msgid "A \"Playing\" activity type." msgstr "プレイ中のアクティビティタイプ。" -#: ../../api.rst:1774 +#: ../../api.rst:1857 msgid "A \"Streaming\" activity type." msgstr "放送中のアクティビティタイプ。" -#: ../../api.rst:1777 +#: ../../api.rst:1860 msgid "A \"Listening\" activity type." msgstr "再生中のアクティビティタイプ。" -#: ../../api.rst:1780 +#: ../../api.rst:1863 msgid "A \"Watching\" activity type." msgstr "視聴中のアクティビティタイプ。" -#: ../../api.rst:1783 +#: ../../api.rst:1866 msgid "A custom activity type." msgstr "カスタムのアクティビティタイプ。" -#: ../../api.rst:1786 +#: ../../api.rst:1869 msgid "A competing activity type." msgstr "競争中のアクティビティタイプ。" -#: ../../api.rst:1792 +#: ../../api.rst:1875 msgid "Specifies a :class:`Guild`\\'s verification level, which is the criteria in which a member must meet before being able to send messages to the guild." msgstr ":class:`Guild` の認証レベルを指定します。これは、メンバーがギルドにメッセージを送信できるようになるまでの条件です。" -#: ../../api.rst:1801 +#: ../../api.rst:1884 msgid "Checks if two verification levels are equal." msgstr "認証レベルが等しいか確認します。" -#: ../../api.rst:1804 +#: ../../api.rst:1887 msgid "Checks if two verification levels are not equal." msgstr "認証レベルが等しくないか確認します。" -#: ../../api.rst:1807 +#: ../../api.rst:1890 msgid "Checks if a verification level is higher than another." msgstr "認証レベルがあるレベルより厳しいか確認します。" -#: ../../api.rst:1810 +#: ../../api.rst:1893 msgid "Checks if a verification level is lower than another." msgstr "認証レベルがあるレベルより緩いか確認します。" -#: ../../api.rst:1813 +#: ../../api.rst:1896 msgid "Checks if a verification level is higher or equal to another." msgstr "認証レベルがあるレベルと同じ、又は厳しいか確認します。" -#: ../../api.rst:1816 +#: ../../api.rst:1899 msgid "Checks if a verification level is lower or equal to another." msgstr "認証レベルがあるレベルと同じ、又は緩いか確認します。" -#: ../../api.rst:1820 +#: ../../api.rst:1903 msgid "No criteria set." msgstr "無制限。" -#: ../../api.rst:1823 +#: ../../api.rst:1906 msgid "Member must have a verified email on their Discord account." msgstr "メンバーはDiscordアカウントのメール認証を済ませないといけません。" -#: ../../api.rst:1826 +#: ../../api.rst:1909 msgid "Member must have a verified email and be registered on Discord for more than five minutes." msgstr "メンバーはメール認証をし、かつアカウント登録から5分経過しないといけません。" -#: ../../api.rst:1830 +#: ../../api.rst:1913 msgid "Member must have a verified email, be registered on Discord for more than five minutes, and be a member of the guild itself for more than ten minutes." msgstr "メンバーはメール認証をし、Discordのアカウント登録から5分経過し、かつ10分以上ギルドに所属していないといけません。" -#: ../../api.rst:1835 +#: ../../api.rst:1918 msgid "Member must have a verified phone on their Discord account." msgstr "メンバーはDiscordアカウントの電話番号認証を済ませないといけません。" -#: ../../api.rst:1839 +#: ../../api.rst:1922 msgid "Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default." msgstr ":class:`Guild` の通知対象のデフォルト設定をすべてのメッセージ、またはメンションのみに指定します。" -#: ../../api.rst:1847 +#: ../../api.rst:1930 msgid "Checks if two notification levels are equal." msgstr "通知レベルが等しいか確認します。" -#: ../../api.rst:1850 +#: ../../api.rst:1933 msgid "Checks if two notification levels are not equal." msgstr "通知レベルが等しくないか確認します。" -#: ../../api.rst:1853 +#: ../../api.rst:1936 msgid "Checks if a notification level is higher than another." msgstr "通知レベルがあるレベルより高いか確認します。" -#: ../../api.rst:1856 +#: ../../api.rst:1939 msgid "Checks if a notification level is lower than another." msgstr "通知レベルがあるレベルより低いか確認します。" -#: ../../api.rst:1859 +#: ../../api.rst:1942 msgid "Checks if a notification level is higher or equal to another." msgstr "通知レベルがあるレベルと同じ、又は高いか確認します。" -#: ../../api.rst:1862 +#: ../../api.rst:1945 msgid "Checks if a notification level is lower or equal to another." msgstr "通知レベルがあるレベルと同じ、又は低いか確認します。" -#: ../../api.rst:1866 +#: ../../api.rst:1949 msgid "Members receive notifications for every message regardless of them being mentioned." msgstr "メンバーは、メンションされているかどうかに関わらず、すべてのメッセージの通知を受け取ります。" -#: ../../api.rst:1869 +#: ../../api.rst:1952 msgid "Members receive notifications for messages they are mentioned in." msgstr "メンバーは自分がメンションされているメッセージの通知のみ受け取ります。" -#: ../../api.rst:1873 +#: ../../api.rst:1956 msgid "Specifies a :class:`Guild`\\'s explicit content filter, which is the machine learning algorithms that Discord uses to detect if an image contains pornography or otherwise explicit content." msgstr ":class:`Guild` の不適切な表現のフィルターを指定します。これはDiscordがポルノ画像や不適切な表現を検出するために使用している機械学習アルゴリズムです。" -#: ../../api.rst:1883 +#: ../../api.rst:1966 msgid "Checks if two content filter levels are equal." msgstr "表現のフィルターのレベルが等しいか確認します。" -#: ../../api.rst:1886 +#: ../../api.rst:1969 msgid "Checks if two content filter levels are not equal." msgstr "表現のフィルターのレベルが等しくないか確認します。" -#: ../../api.rst:1889 +#: ../../api.rst:1972 msgid "Checks if a content filter level is higher than another." msgstr "表現のフィルターのレベルが他のレベルより大きいか確認します。" -#: ../../api.rst:1892 +#: ../../api.rst:1975 msgid "Checks if a content filter level is lower than another." msgstr "表現のフィルターのレベルが他のレベルより小さいか確認します。" -#: ../../api.rst:1895 +#: ../../api.rst:1978 msgid "Checks if a content filter level is higher or equal to another." msgstr "表現のフィルターのレベルが他のレベルより大きい、または等しいか確認します。" -#: ../../api.rst:1898 +#: ../../api.rst:1981 msgid "Checks if a content filter level is lower or equal to another." msgstr "表現のフィルターのレベルが他のレベルより小さい、または等しいか確認します。" -#: ../../api.rst:1902 +#: ../../api.rst:1985 msgid "The guild does not have the content filter enabled." msgstr "ギルドで表現のフィルターが有効ではない。" -#: ../../api.rst:1905 +#: ../../api.rst:1988 msgid "The guild has the content filter enabled for members without a role." msgstr "ギルドでロールを持たないメンバーに対して表現のフィルターが有効化されている。" -#: ../../api.rst:1908 +#: ../../api.rst:1991 msgid "The guild has the content filter enabled for every member." msgstr "ギルドで、すべてのメンバーに対して表現のフィルターが有効化されている。" -#: ../../api.rst:1912 +#: ../../api.rst:1995 msgid "Specifies a :class:`Member` 's status." msgstr ":class:`Member` のステータスを指定します。" -#: ../../api.rst:1916 +#: ../../api.rst:1999 msgid "The member is online." msgstr "メンバーがオンライン。" -#: ../../api.rst:1919 +#: ../../api.rst:2002 msgid "The member is offline." msgstr "メンバーがオフライン。" -#: ../../api.rst:1922 +#: ../../api.rst:2005 msgid "The member is idle." msgstr "メンバーが退席中。" -#: ../../api.rst:1925 +#: ../../api.rst:2008 msgid "The member is \"Do Not Disturb\"." msgstr "メンバーが取り込み中。" -#: ../../api.rst:1928 +#: ../../api.rst:2011 msgid "An alias for :attr:`dnd`." msgstr ":attr:`dnd` のエイリアス。" -#: ../../api.rst:1931 +#: ../../api.rst:2014 msgid "The member is \"invisible\". In reality, this is only used when sending a presence a la :meth:`Client.change_presence`. When you receive a user's presence this will be :attr:`offline` instead." msgstr "メンバーがオンライン状態を隠す。実際には、これは :meth:`Client.change_presence` でプレゼンスを送信する時のみ使用します。ユーザーのプレゼンスを受け取った場合、これは :attr:`offline` に置き換えられます。" -#: ../../api.rst:1938 +#: ../../api.rst:2021 msgid "Represents the type of action being done for a :class:`AuditLogEntry`\\, which is retrievable via :meth:`Guild.audit_logs`." msgstr ":class:`AuditLogEntry` で行われた動作の種類を取得します。AuditLogEntryは :meth:`Guild.audit_logs` で取得可能です。" -#: ../../api.rst:1943 +#: ../../api.rst:2026 msgid "The guild has updated. Things that trigger this include:" msgstr "ギルドの更新。このトリガーとなるものは以下のとおりです。" -#: ../../api.rst:1945 +#: ../../api.rst:2028 msgid "Changing the guild vanity URL" msgstr "ギルドのvanity URLの変更" -#: ../../api.rst:1946 +#: ../../api.rst:2029 msgid "Changing the guild invite splash" msgstr "ギルドの招待時のスプラッシュ画像の変更" -#: ../../api.rst:1947 +#: ../../api.rst:2030 msgid "Changing the guild AFK channel or timeout" msgstr "ギルドのAFKチャンネル、またはタイムアウトの変更" -#: ../../api.rst:1948 +#: ../../api.rst:2031 msgid "Changing the guild voice server region" msgstr "ギルドの音声通話のサーバーリージョンの変更" -#: ../../api.rst:1949 +#: ../../api.rst:2032 msgid "Changing the guild icon, banner, or discovery splash" msgstr "ギルドのアイコン、バナー、ディスカバリースプラッシュの変更" -#: ../../api.rst:1950 +#: ../../api.rst:2033 msgid "Changing the guild moderation settings" msgstr "ギルドの管理設定の変更" -#: ../../api.rst:1951 +#: ../../api.rst:2034 msgid "Changing things related to the guild widget" msgstr "ギルドのウィジェットに関連するものの変更" -#: ../../api.rst:1953 +#: ../../api.rst:2036 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Guild`." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は :class:`Guild` になります。" -#: ../../api.rst:1956 -#: ../../api.rst:1992 -#: ../../api.rst:2011 -#: ../../api.rst:2036 -#: ../../api.rst:2058 +#: ../../api.rst:2039 +#: ../../api.rst:2075 +#: ../../api.rst:2094 +#: ../../api.rst:2119 +#: ../../api.rst:2141 msgid "Possible attributes for :class:`AuditLogDiff`:" msgstr ":class:`AuditLogDiff` から、以下の属性を参照できます:" -#: ../../api.rst:1958 +#: ../../api.rst:2041 msgid ":attr:`~AuditLogDiff.afk_channel`" msgstr ":attr:`~AuditLogDiff.afk_channel`" -#: ../../api.rst:1959 +#: ../../api.rst:2042 msgid ":attr:`~AuditLogDiff.system_channel`" msgstr ":attr:`~AuditLogDiff.system_channel`" -#: ../../api.rst:1960 +#: ../../api.rst:2043 msgid ":attr:`~AuditLogDiff.afk_timeout`" msgstr ":attr:`~AuditLogDiff.afk_timeout`" -#: ../../api.rst:1961 +#: ../../api.rst:2044 msgid ":attr:`~AuditLogDiff.default_notifications`" msgstr ":attr:`~AuditLogDiff.default_notifications`" -#: ../../api.rst:1962 +#: ../../api.rst:2045 msgid ":attr:`~AuditLogDiff.explicit_content_filter`" msgstr ":attr:`~AuditLogDiff.explicit_content_filter`" -#: ../../api.rst:1963 +#: ../../api.rst:2046 msgid ":attr:`~AuditLogDiff.mfa_level`" msgstr ":attr:`~AuditLogDiff.mfa_level`" -#: ../../api.rst:1964 -#: ../../api.rst:1994 -#: ../../api.rst:2013 -#: ../../api.rst:2038 -#: ../../api.rst:2215 +#: ../../api.rst:2047 +#: ../../api.rst:2077 +#: ../../api.rst:2096 +#: ../../api.rst:2121 +#: ../../api.rst:2308 msgid ":attr:`~AuditLogDiff.name`" msgstr ":attr:`~AuditLogDiff.name`" -#: ../../api.rst:1965 +#: ../../api.rst:2048 msgid ":attr:`~AuditLogDiff.owner`" msgstr ":attr:`~AuditLogDiff.owner`" -#: ../../api.rst:1966 +#: ../../api.rst:2049 msgid ":attr:`~AuditLogDiff.splash`" msgstr ":attr:`~AuditLogDiff.splash`" -#: ../../api.rst:1967 +#: ../../api.rst:2050 msgid ":attr:`~AuditLogDiff.discovery_splash`" msgstr ":attr:`~AuditLogDiff.discovery_splash`" -#: ../../api.rst:1968 -#: ../../api.rst:2213 -#: ../../api.rst:2236 +#: ../../api.rst:2051 +#: ../../api.rst:2306 +#: ../../api.rst:2329 msgid ":attr:`~AuditLogDiff.icon`" msgstr ":attr:`~AuditLogDiff.icon`" -#: ../../api.rst:1969 +#: ../../api.rst:2052 msgid ":attr:`~AuditLogDiff.banner`" msgstr ":attr:`~AuditLogDiff.banner`" -#: ../../api.rst:1970 +#: ../../api.rst:2053 msgid ":attr:`~AuditLogDiff.vanity_url_code`" msgstr ":attr:`~AuditLogDiff.vanity_url_code`" -#: ../../api.rst:1971 -#: ../../api.rst:2510 -#: ../../api.rst:2529 -#: ../../api.rst:2548 +#: ../../api.rst:2054 +#: ../../api.rst:2603 +#: ../../api.rst:2622 +#: ../../api.rst:2641 msgid ":attr:`~AuditLogDiff.description`" msgstr ":attr:`~AuditLogDiff.description`" -#: ../../api.rst:1972 +#: ../../api.rst:2055 msgid ":attr:`~AuditLogDiff.preferred_locale`" msgstr ":attr:`~AuditLogDiff.preferred_locale`" -#: ../../api.rst:1973 +#: ../../api.rst:2056 msgid ":attr:`~AuditLogDiff.prune_delete_days`" msgstr ":attr:`~AuditLogDiff.prune_delete_days`" -#: ../../api.rst:1974 +#: ../../api.rst:2057 msgid ":attr:`~AuditLogDiff.public_updates_channel`" msgstr ":attr:`~AuditLogDiff.public_updates_channel`" -#: ../../api.rst:1975 +#: ../../api.rst:2058 msgid ":attr:`~AuditLogDiff.rules_channel`" msgstr ":attr:`~AuditLogDiff.rules_channel`" -#: ../../api.rst:1976 +#: ../../api.rst:2059 msgid ":attr:`~AuditLogDiff.verification_level`" msgstr ":attr:`~AuditLogDiff.verification_level`" -#: ../../api.rst:1977 +#: ../../api.rst:2060 msgid ":attr:`~AuditLogDiff.widget_channel`" msgstr ":attr:`~AuditLogDiff.widget_channel`" -#: ../../api.rst:1978 +#: ../../api.rst:2061 msgid ":attr:`~AuditLogDiff.widget_enabled`" msgstr ":attr:`~AuditLogDiff.widget_enabled`" -#: ../../api.rst:1979 +#: ../../api.rst:2062 msgid ":attr:`~AuditLogDiff.premium_progress_bar_enabled`" msgstr ":attr:`~AuditLogDiff.premium_progress_bar_enabled`" -#: ../../api.rst:1980 +#: ../../api.rst:2063 msgid ":attr:`~AuditLogDiff.system_channel_flags`" msgstr ":attr:`~AuditLogDiff.system_channel_flags`" -#: ../../api.rst:1984 +#: ../../api.rst:2067 msgid "A new channel was created." msgstr "チャンネルの作成。" -#: ../../api.rst:1986 +#: ../../api.rst:2069 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is either a :class:`abc.GuildChannel` or :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`abc.GuildChannel` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:1989 +#: ../../api.rst:2072 msgid "A more filled out object in the :class:`Object` case can be found by using :attr:`~AuditLogEntry.after`." msgstr ":class:`Object` の場合、 :attr:`~AuditLogEntry.after` を使用して、より詳細な情報を持つオブジェクトを見つけることができます。" -#: ../../api.rst:1995 -#: ../../api.rst:2014 -#: ../../api.rst:2039 -#: ../../api.rst:2063 -#: ../../api.rst:2079 +#: ../../api.rst:2078 +#: ../../api.rst:2097 +#: ../../api.rst:2122 +#: ../../api.rst:2146 +#: ../../api.rst:2162 msgid ":attr:`~AuditLogDiff.type`" msgstr ":attr:`~AuditLogDiff.type`" -#: ../../api.rst:1996 -#: ../../api.rst:2016 -#: ../../api.rst:2040 +#: ../../api.rst:2079 +#: ../../api.rst:2099 +#: ../../api.rst:2123 msgid ":attr:`~AuditLogDiff.overwrites`" msgstr ":attr:`~AuditLogDiff.overwrites`" -#: ../../api.rst:2000 +#: ../../api.rst:2083 msgid "A channel was updated. Things that trigger this include:" msgstr "チャンネルの更新。これのトリガーとなるものは以下の通りです。" -#: ../../api.rst:2002 +#: ../../api.rst:2085 msgid "The channel name or topic was changed" msgstr "チャンネルのチャンネルトピックの変更、または名前の変更。" -#: ../../api.rst:2003 +#: ../../api.rst:2086 msgid "The channel bitrate was changed" msgstr "チャンネルのビットレートの変更。" -#: ../../api.rst:2005 -#: ../../api.rst:2049 +#: ../../api.rst:2088 +#: ../../api.rst:2132 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`abc.GuildChannel` or :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`abc.GuildChannel` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:2008 +#: ../../api.rst:2091 msgid "A more filled out object in the :class:`Object` case can be found by using :attr:`~AuditLogEntry.after` or :attr:`~AuditLogEntry.before`." msgstr ":class:`Object` の場合、 :attr:`~AuditLogEntry.after` または :attr:`~AuditLogEntry.before` を使用して、より詳細な情報を持つオブジェクトを見つけることができます。" -#: ../../api.rst:2015 +#: ../../api.rst:2098 msgid ":attr:`~AuditLogDiff.position`" msgstr ":attr:`~AuditLogDiff.position`" -#: ../../api.rst:2017 -#: ../../api.rst:2470 -#: ../../api.rst:2485 +#: ../../api.rst:2100 +#: ../../api.rst:2563 +#: ../../api.rst:2578 msgid ":attr:`~AuditLogDiff.topic`" msgstr ":attr:`~AuditLogDiff.topic`" -#: ../../api.rst:2018 +#: ../../api.rst:2101 msgid ":attr:`~AuditLogDiff.bitrate`" msgstr ":attr:`~AuditLogDiff.bitrate`" -#: ../../api.rst:2019 +#: ../../api.rst:2102 msgid ":attr:`~AuditLogDiff.rtc_region`" msgstr ":attr:`~AuditLogDiff.rtc_region`" -#: ../../api.rst:2020 +#: ../../api.rst:2103 msgid ":attr:`~AuditLogDiff.video_quality_mode`" msgstr ":attr:`~AuditLogDiff.video_quality_mode`" -#: ../../api.rst:2021 +#: ../../api.rst:2104 msgid ":attr:`~AuditLogDiff.default_auto_archive_duration`" msgstr ":attr:`~AuditLogDiff.default_auto_archive_duration`" -#: ../../api.rst:2022 -#: ../../api.rst:2042 +#: ../../api.rst:2105 +#: ../../api.rst:2125 msgid ":attr:`~AuditLogDiff.nsfw`" msgstr ":attr:`~AuditLogDiff.nsfw`" -#: ../../api.rst:2023 -#: ../../api.rst:2043 +#: ../../api.rst:2106 +#: ../../api.rst:2126 msgid ":attr:`~AuditLogDiff.slowmode_delay`" msgstr ":attr:`~AuditLogDiff.slowmode_delay`" -#: ../../api.rst:2024 +#: ../../api.rst:2107 msgid ":attr:`~AuditLogDiff.user_limit`" msgstr ":attr:`~AuditLogDiff.user_limit`" -#: ../../api.rst:2028 +#: ../../api.rst:2111 msgid "A channel was deleted." msgstr "チャンネルの削除。" -#: ../../api.rst:2030 +#: ../../api.rst:2113 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is an :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2033 +#: ../../api.rst:2116 msgid "A more filled out object can be found by using the :attr:`~AuditLogEntry.before` object." msgstr ":attr:`~AuditLogEntry.before` オブジェクトを使用すると、より詳細な情報が見つかります。" -#: ../../api.rst:2041 +#: ../../api.rst:2124 msgid ":attr:`~AuditLogDiff.flags`" msgstr ":attr:`~AuditLogDiff.flags`" -#: ../../api.rst:2047 +#: ../../api.rst:2130 msgid "A channel permission overwrite was created." msgstr "チャンネルにおける権限の上書き設定の作成。" -#: ../../api.rst:2052 +#: ../../api.rst:2135 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is either a :class:`Role` or :class:`Member`. If the object is not found then it is a :class:`Object` with an ID being filled, a name, and a ``type`` attribute set to either ``'role'`` or ``'member'`` to help dictate what type of ID it is." msgstr "この場合には、 :attr:`~AuditLogEntry.extra` は :class:`Role` か :class:`Member` です。もしオブジェクトが見つからない場合はid、name、 ``'role'`` か ``'member'`` である ``type`` 属性がある :class:`Object` です。" -#: ../../api.rst:2060 -#: ../../api.rst:2076 -#: ../../api.rst:2091 +#: ../../api.rst:2143 +#: ../../api.rst:2159 +#: ../../api.rst:2174 msgid ":attr:`~AuditLogDiff.deny`" msgstr ":attr:`~AuditLogDiff.deny`" -#: ../../api.rst:2061 -#: ../../api.rst:2077 -#: ../../api.rst:2092 +#: ../../api.rst:2144 +#: ../../api.rst:2160 +#: ../../api.rst:2175 msgid ":attr:`~AuditLogDiff.allow`" msgstr ":attr:`~AuditLogDiff.allow`" -#: ../../api.rst:2062 -#: ../../api.rst:2078 -#: ../../api.rst:2093 +#: ../../api.rst:2145 +#: ../../api.rst:2161 +#: ../../api.rst:2176 msgid ":attr:`~AuditLogDiff.id`" msgstr ":attr:`~AuditLogDiff.id`" -#: ../../api.rst:2067 +#: ../../api.rst:2150 msgid "A channel permission overwrite was changed, this is typically when the permission values change." msgstr "チャンネルの権限の上書きの変更。典型的な例は、権限が変更された場合です。" -#: ../../api.rst:2070 -#: ../../api.rst:2085 +#: ../../api.rst:2153 +#: ../../api.rst:2168 msgid "See :attr:`overwrite_create` for more information on how the :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields are set." msgstr ":attr:`overwrite_create` に、 :attr:`~AuditLogEntry.target` と :attr:`~AuditLogEntry.extra` についての説明があります。" -#: ../../api.rst:2083 +#: ../../api.rst:2166 msgid "A channel permission overwrite was deleted." msgstr "チャンネルにおける権限の上書き設定の削除。" -#: ../../api.rst:2098 +#: ../../api.rst:2181 msgid "A member was kicked." msgstr "メンバーのキック。" -#: ../../api.rst:2100 +#: ../../api.rst:2183 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got kicked." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、キックされた :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2103 -#: ../../api.rst:2118 -#: ../../api.rst:2127 -#: ../../api.rst:2136 +#: ../../api.rst:2186 +#: ../../api.rst:2251 +#: ../../api.rst:2278 +#: ../../api.rst:2486 +msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with one attribute:" +msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" + +#: ../../api.rst:2189 +#: ../../api.rst:2254 +msgid "``integration_type``: An optional string that denotes the type of integration that did the action." +msgstr "" + +#: ../../api.rst:2191 +#: ../../api.rst:2206 +#: ../../api.rst:2215 +#: ../../api.rst:2224 msgid "When this is the action, :attr:`~AuditLogEntry.changes` is empty." msgstr "これが上記のactionなら、:attr:`~AuditLogEntry.changes` は空になります。" -#: ../../api.rst:2107 +#: ../../api.rst:2195 msgid "A member prune was triggered." msgstr "非アクティブメンバーの一括キック。" -#: ../../api.rst:2109 +#: ../../api.rst:2197 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is set to ``None``." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は ``None`` に設定されます。" -#: ../../api.rst:2112 -#: ../../api.rst:2172 -#: ../../api.rst:2380 -#: ../../api.rst:2407 -#: ../../api.rst:2422 +#: ../../api.rst:2200 +#: ../../api.rst:2265 +#: ../../api.rst:2473 +#: ../../api.rst:2500 +#: ../../api.rst:2515 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with two attributes:" msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" -#: ../../api.rst:2115 +#: ../../api.rst:2203 msgid "``delete_member_days``: An integer specifying how far the prune was." msgstr "``delete_members_days`` : 一括キック対象の期間を示す整数。" -#: ../../api.rst:2116 +#: ../../api.rst:2204 msgid "``members_removed``: An integer specifying how many members were removed." msgstr "``members_removed`` : 除去されたメンバーの数を示す整数。" -#: ../../api.rst:2122 +#: ../../api.rst:2210 msgid "A member was banned." msgstr "メンバーのBAN。" -#: ../../api.rst:2124 +#: ../../api.rst:2212 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got banned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、BANされた :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2131 +#: ../../api.rst:2219 msgid "A member was unbanned." msgstr "メンバーのBANの解除。" -#: ../../api.rst:2133 +#: ../../api.rst:2221 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got unbanned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、BAN解除された :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2140 +#: ../../api.rst:2228 msgid "A member has updated. This triggers in the following situations:" msgstr "メンバーの何らかの更新。これのトリガーとなるのは以下の場合です:" -#: ../../api.rst:2142 +#: ../../api.rst:2230 msgid "A nickname was changed" msgstr "メンバーのニックネームの変更。" -#: ../../api.rst:2143 +#: ../../api.rst:2231 msgid "They were server muted or deafened (or it was undo'd)" msgstr "サーバー側でミュートやスピーカーミュートされた(あるいは解除された)場合。" -#: ../../api.rst:2145 +#: ../../api.rst:2233 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who got updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、更新の行われた :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2150 +#: ../../api.rst:2238 msgid ":attr:`~AuditLogDiff.nick`" msgstr ":attr:`~AuditLogDiff.nick`" -#: ../../api.rst:2151 +#: ../../api.rst:2239 msgid ":attr:`~AuditLogDiff.mute`" msgstr ":attr:`~AuditLogDiff.mute`" -#: ../../api.rst:2152 +#: ../../api.rst:2240 msgid ":attr:`~AuditLogDiff.deaf`" msgstr ":attr:`~AuditLogDiff.deaf`" -#: ../../api.rst:2153 +#: ../../api.rst:2241 msgid ":attr:`~AuditLogDiff.timed_out_until`" msgstr ":attr:`~AuditLogDiff.timed_out_until`" -#: ../../api.rst:2157 +#: ../../api.rst:2245 msgid "A member's role has been updated. This triggers when a member either gains a role or loses a role." msgstr "メンバーのロールの更新。これは、メンバーがロールを得たり、失った場合に発生します。" -#: ../../api.rst:2160 +#: ../../api.rst:2248 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who got the role." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ロールの更新が行われた :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2165 +#: ../../api.rst:2258 msgid ":attr:`~AuditLogDiff.roles`" msgstr ":attr:`~AuditLogDiff.roles`" -#: ../../api.rst:2169 +#: ../../api.rst:2262 msgid "A member's voice channel has been updated. This triggers when a member is moved to a different voice channel." msgstr "メンバーのボイスチャンネルの更新。これは、メンバーが他のボイスチャンネルに移動させられた時に発生します。" -#: ../../api.rst:2175 +#: ../../api.rst:2268 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the members were moved." msgstr "``channel`` : メンバーの移動先の :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2176 +#: ../../api.rst:2269 msgid "``count``: An integer specifying how many members were moved." msgstr "``count`` : 移動されたメンバーの数を示す整数。" -#: ../../api.rst:2182 +#: ../../api.rst:2275 msgid "A member's voice state has changed. This triggers when a member is force disconnected from voice." msgstr "メンバーのボイス状態の変更。これはメンバーがボイスから強制的に切断された場合に発生します。" -#: ../../api.rst:2185 -#: ../../api.rst:2393 -msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with one attribute:" -msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" - -#: ../../api.rst:2188 +#: ../../api.rst:2281 msgid "``count``: An integer specifying how many members were disconnected." msgstr "``count`` : 切断されたメンバーの数を示す整数。" -#: ../../api.rst:2194 +#: ../../api.rst:2287 msgid "A bot was added to the guild." msgstr "ボットのギルドへの追加。" -#: ../../api.rst:2196 +#: ../../api.rst:2289 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` which was added to the guild." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ギルドに追加された :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2203 +#: ../../api.rst:2296 msgid "A new role was created." msgstr "新しいロールの作成。" -#: ../../api.rst:2205 -#: ../../api.rst:2228 -#: ../../api.rst:2245 +#: ../../api.rst:2298 +#: ../../api.rst:2321 +#: ../../api.rst:2338 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Role` or a :class:`Object` with the ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`Role` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:2210 -#: ../../api.rst:2233 -#: ../../api.rst:2250 +#: ../../api.rst:2303 +#: ../../api.rst:2326 +#: ../../api.rst:2343 msgid ":attr:`~AuditLogDiff.colour`" msgstr ":attr:`~AuditLogDiff.colour`" -#: ../../api.rst:2211 -#: ../../api.rst:2234 -#: ../../api.rst:2251 +#: ../../api.rst:2304 +#: ../../api.rst:2327 +#: ../../api.rst:2344 msgid ":attr:`~AuditLogDiff.mentionable`" msgstr ":attr:`~AuditLogDiff.mentionable`" -#: ../../api.rst:2212 -#: ../../api.rst:2235 -#: ../../api.rst:2252 +#: ../../api.rst:2305 +#: ../../api.rst:2328 +#: ../../api.rst:2345 msgid ":attr:`~AuditLogDiff.hoist`" msgstr ":attr:`~AuditLogDiff.hoist`" -#: ../../api.rst:2214 -#: ../../api.rst:2237 +#: ../../api.rst:2307 +#: ../../api.rst:2330 msgid ":attr:`~AuditLogDiff.unicode_emoji`" msgstr ":attr:`~AuditLogDiff.unicode_emoji`" -#: ../../api.rst:2216 -#: ../../api.rst:2239 -#: ../../api.rst:2254 +#: ../../api.rst:2309 +#: ../../api.rst:2332 +#: ../../api.rst:2347 msgid ":attr:`~AuditLogDiff.permissions`" msgstr ":attr:`~AuditLogDiff.permissions`" -#: ../../api.rst:2220 +#: ../../api.rst:2313 msgid "A role was updated. This triggers in the following situations:" msgstr "ロールの何らかの更新。これのトリガーとなるのは以下の場合です:" -#: ../../api.rst:2222 +#: ../../api.rst:2315 msgid "The name has changed" msgstr "名前の更新。" -#: ../../api.rst:2223 +#: ../../api.rst:2316 msgid "The permissions have changed" msgstr "権限の更新。" -#: ../../api.rst:2224 +#: ../../api.rst:2317 msgid "The colour has changed" msgstr "色の更新。" -#: ../../api.rst:2225 +#: ../../api.rst:2318 msgid "The role icon (or unicode emoji) has changed" msgstr "ロールアイコン (または Unicode 絵文字)の変更。" -#: ../../api.rst:2226 +#: ../../api.rst:2319 msgid "Its hoist/mentionable state has changed" msgstr "ロールメンバーのオンライン表示、ロールへのメンションの許可の変更。" -#: ../../api.rst:2243 +#: ../../api.rst:2336 msgid "A role was deleted." msgstr "ロールの削除。" -#: ../../api.rst:2258 +#: ../../api.rst:2351 msgid "An invite was created." msgstr "招待の作成。" -#: ../../api.rst:2260 +#: ../../api.rst:2353 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was created." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は作成された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2265 -#: ../../api.rst:2289 +#: ../../api.rst:2358 +#: ../../api.rst:2382 msgid ":attr:`~AuditLogDiff.max_age`" msgstr ":attr:`~AuditLogDiff.max_age`" -#: ../../api.rst:2266 -#: ../../api.rst:2290 +#: ../../api.rst:2359 +#: ../../api.rst:2383 msgid ":attr:`~AuditLogDiff.code`" msgstr ":attr:`~AuditLogDiff.code`" -#: ../../api.rst:2267 -#: ../../api.rst:2291 +#: ../../api.rst:2360 +#: ../../api.rst:2384 msgid ":attr:`~AuditLogDiff.temporary`" msgstr ":attr:`~AuditLogDiff.temporary`" -#: ../../api.rst:2268 -#: ../../api.rst:2292 +#: ../../api.rst:2361 +#: ../../api.rst:2385 msgid ":attr:`~AuditLogDiff.inviter`" msgstr ":attr:`~AuditLogDiff.inviter`" -#: ../../api.rst:2269 -#: ../../api.rst:2293 -#: ../../api.rst:2306 -#: ../../api.rst:2322 -#: ../../api.rst:2335 +#: ../../api.rst:2362 +#: ../../api.rst:2386 +#: ../../api.rst:2399 +#: ../../api.rst:2415 +#: ../../api.rst:2428 msgid ":attr:`~AuditLogDiff.channel`" msgstr ":attr:`~AuditLogDiff.channel`" -#: ../../api.rst:2270 -#: ../../api.rst:2294 +#: ../../api.rst:2363 +#: ../../api.rst:2387 msgid ":attr:`~AuditLogDiff.uses`" msgstr ":attr:`~AuditLogDiff.uses`" -#: ../../api.rst:2271 -#: ../../api.rst:2295 +#: ../../api.rst:2364 +#: ../../api.rst:2388 msgid ":attr:`~AuditLogDiff.max_uses`" msgstr ":attr:`~AuditLogDiff.max_uses`" -#: ../../api.rst:2275 +#: ../../api.rst:2368 msgid "An invite was updated." msgstr "招待の更新。" -#: ../../api.rst:2277 +#: ../../api.rst:2370 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was updated." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は更新された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2282 +#: ../../api.rst:2375 msgid "An invite was deleted." msgstr "招待の削除。" -#: ../../api.rst:2284 +#: ../../api.rst:2377 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was deleted." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` のタイプは削除された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2299 +#: ../../api.rst:2392 msgid "A webhook was created." msgstr "Webhookの作成。" -#: ../../api.rst:2301 -#: ../../api.rst:2317 -#: ../../api.rst:2330 +#: ../../api.rst:2394 +#: ../../api.rst:2410 +#: ../../api.rst:2423 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Object` with the webhook ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のタイプは、webhook IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2308 -#: ../../api.rst:2337 +#: ../../api.rst:2401 +#: ../../api.rst:2430 msgid ":attr:`~AuditLogDiff.type` (always set to ``1`` if so)" msgstr ":attr:`~AuditLogDiff.type` (その場合は常に ``1`` です。)" -#: ../../api.rst:2312 +#: ../../api.rst:2405 msgid "A webhook was updated. This trigger in the following situations:" msgstr "Webhookの更新。これのトリガーとなるのは以下の場合です。" -#: ../../api.rst:2314 +#: ../../api.rst:2407 msgid "The webhook name changed" msgstr "Webhook名が変更されたとき" -#: ../../api.rst:2315 +#: ../../api.rst:2408 msgid "The webhook channel changed" msgstr "Webhookチャンネルが変更されたとき" -#: ../../api.rst:2324 +#: ../../api.rst:2417 msgid ":attr:`~AuditLogDiff.avatar`" msgstr ":attr:`~AuditLogDiff.avatar`" -#: ../../api.rst:2328 +#: ../../api.rst:2421 msgid "A webhook was deleted." msgstr "Webhookの削除。" -#: ../../api.rst:2341 +#: ../../api.rst:2434 msgid "An emoji was created." msgstr "絵文字の作成。" -#: ../../api.rst:2343 -#: ../../api.rst:2354 +#: ../../api.rst:2436 +#: ../../api.rst:2447 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Emoji` or :class:`Object` with the emoji ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Emoji` または 絵文字IDが設定された :class:`Object` です。" -#: ../../api.rst:2352 +#: ../../api.rst:2445 msgid "An emoji was updated. This triggers when the name has changed." msgstr "絵文字に対する何らかの更新。これは名前が変更されたときに発生します。" -#: ../../api.rst:2363 +#: ../../api.rst:2456 msgid "An emoji was deleted." msgstr "絵文字の削除。" -#: ../../api.rst:2365 +#: ../../api.rst:2458 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Object` with the emoji ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、絵文字IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2374 +#: ../../api.rst:2467 msgid "A message was deleted by a moderator. Note that this only triggers if the message was deleted by someone other than the author." msgstr "管理者によるメッセージの削除。なお、これのトリガーとなるのは、メッセージが投稿者以外によって削除された場合のみです。" -#: ../../api.rst:2377 +#: ../../api.rst:2470 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、削除されたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2383 -#: ../../api.rst:2396 +#: ../../api.rst:2476 +#: ../../api.rst:2489 msgid "``count``: An integer specifying how many messages were deleted." msgstr "``count`` : 削除されたメッセージの数を示す整数。" -#: ../../api.rst:2384 +#: ../../api.rst:2477 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message got deleted." msgstr "``channel`` : メッセージが削除された :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2388 +#: ../../api.rst:2481 msgid "Messages were bulk deleted by a moderator." msgstr "管理者によるメッセージの一括削除。" -#: ../../api.rst:2390 +#: ../../api.rst:2483 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`TextChannel` or :class:`Object` with the ID of the channel that was purged." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`TextChannel` またはメッセージが一括削除されたチャンネルIDが設定された :class:`Object` です。" -#: ../../api.rst:2402 +#: ../../api.rst:2495 msgid "A message was pinned in a channel." msgstr "チャンネルへのメッセージのピン留め。" -#: ../../api.rst:2404 +#: ../../api.rst:2497 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message pinned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ピン留めされたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2410 +#: ../../api.rst:2503 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message was pinned." msgstr "``channel`` : メッセージがピン留めされた :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2411 +#: ../../api.rst:2504 msgid "``message_id``: the ID of the message which was pinned." msgstr "``message_id`` : ピン留めされたメッセージのID。" -#: ../../api.rst:2417 +#: ../../api.rst:2510 msgid "A message was unpinned in a channel." msgstr "チャンネルからのメッセージのピン留め解除。" -#: ../../api.rst:2419 +#: ../../api.rst:2512 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message unpinned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ピン留めが外されたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2425 +#: ../../api.rst:2518 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message was unpinned." msgstr "``channel`` : メッセージのピン留めが外された :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2426 +#: ../../api.rst:2519 msgid "``message_id``: the ID of the message which was unpinned." msgstr "``message_id`` : ピン留めが外されたメッセージのID。" -#: ../../api.rst:2432 +#: ../../api.rst:2525 msgid "A guild integration was created." msgstr "ギルドの連携サービスの作成。" -#: ../../api.rst:2434 +#: ../../api.rst:2527 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または作成されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2442 +#: ../../api.rst:2535 msgid "A guild integration was updated." msgstr "ギルド連携サービスの更新。" -#: ../../api.rst:2444 +#: ../../api.rst:2537 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または更新されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2452 +#: ../../api.rst:2545 msgid "A guild integration was deleted." msgstr "ギルド連携サービスの削除。" -#: ../../api.rst:2454 +#: ../../api.rst:2547 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または削除されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2462 +#: ../../api.rst:2555 msgid "A stage instance was started." msgstr "ステージインスタンスの開始。" -#: ../../api.rst:2464 +#: ../../api.rst:2557 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`StageInstance` or :class:`Object` with the ID of the stage instance which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`StageInstance` または作成されたステージインスタンスのIDが設定された :class:`Object` です。" -#: ../../api.rst:2471 -#: ../../api.rst:2486 +#: ../../api.rst:2564 +#: ../../api.rst:2579 msgid ":attr:`~AuditLogDiff.privacy_level`" msgstr ":attr:`~AuditLogDiff.privacy_level`" -#: ../../api.rst:2477 +#: ../../api.rst:2570 msgid "A stage instance was updated." msgstr "ステージインスタンスの更新。" -#: ../../api.rst:2479 +#: ../../api.rst:2572 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`StageInstance` or :class:`Object` with the ID of the stage instance which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`StageInstance` または更新されたステージインスタンスのIDが設定された :class:`Object` です。" -#: ../../api.rst:2492 +#: ../../api.rst:2585 msgid "A stage instance was ended." msgstr "ステージインスタンスの終了。" -#: ../../api.rst:2498 +#: ../../api.rst:2591 msgid "A sticker was created." msgstr "スタンプの作成。" -#: ../../api.rst:2500 +#: ../../api.rst:2593 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`GuildSticker` or :class:`Object` with the ID of the sticker which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`GuildSticker` または作成されたスタンプのIDが設定された :class:`Object` です。" -#: ../../api.rst:2507 -#: ../../api.rst:2526 -#: ../../api.rst:2545 +#: ../../api.rst:2600 +#: ../../api.rst:2619 +#: ../../api.rst:2638 msgid ":attr:`~AuditLogDiff.emoji`" msgstr ":attr:`~AuditLogDiff.emoji`" -#: ../../api.rst:2509 -#: ../../api.rst:2528 -#: ../../api.rst:2547 +#: ../../api.rst:2602 +#: ../../api.rst:2621 +#: ../../api.rst:2640 msgid ":attr:`~AuditLogDiff.format_type`" msgstr ":attr:`~AuditLogDiff.format_type`" -#: ../../api.rst:2511 -#: ../../api.rst:2530 -#: ../../api.rst:2549 +#: ../../api.rst:2604 +#: ../../api.rst:2623 +#: ../../api.rst:2642 msgid ":attr:`~AuditLogDiff.available`" msgstr ":attr:`~AuditLogDiff.available`" -#: ../../api.rst:2517 +#: ../../api.rst:2610 msgid "A sticker was updated." msgstr "スタンプの更新。" -#: ../../api.rst:2519 -#: ../../api.rst:2538 +#: ../../api.rst:2612 +#: ../../api.rst:2631 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`GuildSticker` or :class:`Object` with the ID of the sticker which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`GuildSticker` または更新されたスタンプのIDが設定された :class:`Object` です。" -#: ../../api.rst:2536 +#: ../../api.rst:2629 msgid "A sticker was deleted." msgstr "スタンプの削除。" -#: ../../api.rst:2555 -#: ../../api.rst:2574 -#: ../../api.rst:2593 +#: ../../api.rst:2648 +#: ../../api.rst:2667 +#: ../../api.rst:2686 msgid "A scheduled event was created." msgstr "スケジュールイベントの作成。" -#: ../../api.rst:2557 +#: ../../api.rst:2650 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または作成されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2561 -#: ../../api.rst:2580 -#: ../../api.rst:2599 +#: ../../api.rst:2654 +#: ../../api.rst:2673 +#: ../../api.rst:2692 msgid "Possible attributes for :class:`AuditLogDiff`: - :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.description` - :attr:`~AuditLogDiff.privacy_level` - :attr:`~AuditLogDiff.status` - :attr:`~AuditLogDiff.entity_type` - :attr:`~AuditLogDiff.cover_image`" msgstr ":class:`AuditLogDiff` の可能な属性: - :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.description` - :attr:`~AuditLogDiff.privacy_level` - :attr:`~AuditLogDiff.status` - :attr:`~AuditLogDiff.entity_type` - :attr:`~AuditLogDiff.cover_image`" -#: ../../api.rst:2576 +#: ../../api.rst:2669 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または更新されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2595 +#: ../../api.rst:2688 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または削除されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2612 +#: ../../api.rst:2705 msgid "A thread was created." msgstr "スレッドの作成。" -#: ../../api.rst:2614 +#: ../../api.rst:2707 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または作成されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2621 -#: ../../api.rst:2639 -#: ../../api.rst:2657 +#: ../../api.rst:2714 +#: ../../api.rst:2732 +#: ../../api.rst:2750 msgid ":attr:`~AuditLogDiff.archived`" msgstr ":attr:`~AuditLogDiff.archived`" -#: ../../api.rst:2622 -#: ../../api.rst:2640 -#: ../../api.rst:2658 +#: ../../api.rst:2715 +#: ../../api.rst:2733 +#: ../../api.rst:2751 msgid ":attr:`~AuditLogDiff.locked`" msgstr ":attr:`~AuditLogDiff.locked`" -#: ../../api.rst:2623 -#: ../../api.rst:2641 -#: ../../api.rst:2659 +#: ../../api.rst:2716 +#: ../../api.rst:2734 +#: ../../api.rst:2752 msgid ":attr:`~AuditLogDiff.auto_archive_duration`" msgstr ":attr:`~AuditLogDiff.auto_archive_duration`" -#: ../../api.rst:2624 -#: ../../api.rst:2642 -#: ../../api.rst:2660 +#: ../../api.rst:2717 +#: ../../api.rst:2735 +#: ../../api.rst:2753 msgid ":attr:`~AuditLogDiff.invitable`" msgstr ":attr:`~AuditLogDiff.invitable`" -#: ../../api.rst:2630 +#: ../../api.rst:2723 msgid "A thread was updated." msgstr "スレッドの更新。" -#: ../../api.rst:2632 +#: ../../api.rst:2725 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または更新されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2648 +#: ../../api.rst:2741 msgid "A thread was deleted." msgstr "スレッドの削除。" -#: ../../api.rst:2650 +#: ../../api.rst:2743 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または削除されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2666 +#: ../../api.rst:2759 msgid "An application command or integrations application command permissions were updated." msgstr "アプリケーションコマンドまたはインテグレーションアプリケーションコマンドの権限の更新。" -#: ../../api.rst:2669 +#: ../../api.rst:2762 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` for an integrations general permissions, :class:`~discord.app_commands.AppCommand` for a specific commands permissions, or :class:`Object` with the ID of the command or integration which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 インテグレーション一般の権限の場合 :class:`PartialIntegration` 、特定のコマンドの権限の場合 :class:`~discord.app_commands.AppCommand` 、あるいは更新されたコマンドまたはインテグレーションのIDが設定された :class:`Object` です。" -#: ../../api.rst:2675 +#: ../../api.rst:2768 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an :class:`PartialIntegration` or :class:`Object` with the ID of application that command or integration belongs to." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` の型は、 :class:`PartialIntegration` またはコマンドまたはインテグレーションが属するアプリケーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2681 +#: ../../api.rst:2774 msgid ":attr:`~AuditLogDiff.app_command_permissions`" msgstr ":attr:`~AuditLogDiff.app_command_permissions`" -#: ../../api.rst:2687 +#: ../../api.rst:2780 msgid "An automod rule was created." msgstr "自動管理ルールの作成。" -#: ../../api.rst:2689 -#: ../../api.rst:2710 -#: ../../api.rst:2731 +#: ../../api.rst:2782 +#: ../../api.rst:2803 +#: ../../api.rst:2824 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`AutoModRule` or :class:`Object` with the ID of the automod rule that was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のtypeは、 :class:`AutoModRule` または作成された自動管理ルールのIDが設定された :class:`Object` です。" -#: ../../api.rst:2696 -#: ../../api.rst:2717 -#: ../../api.rst:2738 +#: ../../api.rst:2789 +#: ../../api.rst:2810 +#: ../../api.rst:2831 msgid ":attr:`~AuditLogDiff.enabled`" msgstr ":attr:`~AuditLogDiff.enabled`" -#: ../../api.rst:2697 -#: ../../api.rst:2718 -#: ../../api.rst:2739 +#: ../../api.rst:2790 +#: ../../api.rst:2811 +#: ../../api.rst:2832 msgid ":attr:`~AuditLogDiff.event_type`" msgstr ":attr:`~AuditLogDiff.event_type`" -#: ../../api.rst:2698 -#: ../../api.rst:2719 -#: ../../api.rst:2740 +#: ../../api.rst:2791 +#: ../../api.rst:2812 +#: ../../api.rst:2833 msgid ":attr:`~AuditLogDiff.trigger_type`" msgstr ":attr:`~AuditLogDiff.trigger_type`" -#: ../../api.rst:2699 -#: ../../api.rst:2720 -#: ../../api.rst:2741 +#: ../../api.rst:2792 +#: ../../api.rst:2813 +#: ../../api.rst:2834 msgid ":attr:`~AuditLogDiff.trigger`" msgstr ":attr:`~AuditLogDiff.trigger`" -#: ../../api.rst:2700 -#: ../../api.rst:2721 -#: ../../api.rst:2742 +#: ../../api.rst:2793 +#: ../../api.rst:2814 +#: ../../api.rst:2835 msgid ":attr:`~AuditLogDiff.actions`" msgstr ":attr:`~AuditLogDiff.actions`" -#: ../../api.rst:2701 -#: ../../api.rst:2722 -#: ../../api.rst:2743 +#: ../../api.rst:2794 +#: ../../api.rst:2815 +#: ../../api.rst:2836 msgid ":attr:`~AuditLogDiff.exempt_roles`" msgstr ":attr:`~AuditLogDiff.exempt_roles`" -#: ../../api.rst:2702 -#: ../../api.rst:2723 -#: ../../api.rst:2744 +#: ../../api.rst:2795 +#: ../../api.rst:2816 +#: ../../api.rst:2837 msgid ":attr:`~AuditLogDiff.exempt_channels`" msgstr ":attr:`~AuditLogDiff.exempt_channels`" -#: ../../api.rst:2708 +#: ../../api.rst:2801 msgid "An automod rule was updated." msgstr "自動管理ルールの更新。" -#: ../../api.rst:2729 +#: ../../api.rst:2822 msgid "An automod rule was deleted." msgstr "自動管理ルールの削除。" -#: ../../api.rst:2750 +#: ../../api.rst:2843 msgid "An automod rule blocked a message from being sent." msgstr "自動管理ルールによる送信されたメッセージのブロック。" -#: ../../api.rst:2752 -#: ../../api.rst:2770 -#: ../../api.rst:2788 +#: ../../api.rst:2845 +#: ../../api.rst:2863 +#: ../../api.rst:2881 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`Member` with the ID of the person who triggered the automod rule." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のtypeは、自動管理ルールを発動させた :class:`Member` になります。" -#: ../../api.rst:2755 -#: ../../api.rst:2773 -#: ../../api.rst:2791 +#: ../../api.rst:2848 +#: ../../api.rst:2866 +#: ../../api.rst:2884 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with 3 attributes:" msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の3つの属性を持つプロキシオブジェクトになります:" -#: ../../api.rst:2758 -#: ../../api.rst:2776 -#: ../../api.rst:2794 +#: ../../api.rst:2851 +#: ../../api.rst:2869 +#: ../../api.rst:2887 msgid "``automod_rule_name``: The name of the automod rule that was triggered." msgstr "``automod_rule_name`` : 発動した自動管理ルールの名前。" -#: ../../api.rst:2759 -#: ../../api.rst:2777 -#: ../../api.rst:2795 -msgid "``automod_rule_trigger``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered." -msgstr "``automod_rule_trigger`` : 発動されたルールの :class:`AutoModRuleTriggerType` 。" +#: ../../api.rst:2852 +#: ../../api.rst:2870 +#: ../../api.rst:2888 +msgid "``automod_rule_trigger_type``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered." +msgstr "" -#: ../../api.rst:2760 -#: ../../api.rst:2778 -#: ../../api.rst:2796 +#: ../../api.rst:2853 +#: ../../api.rst:2871 +#: ../../api.rst:2889 msgid "``channel``: The channel in which the automod rule was triggered." msgstr "``channel`` : 自動管理ルールが発動されたチャンネル。" -#: ../../api.rst:2762 -#: ../../api.rst:2780 -#: ../../api.rst:2798 +#: ../../api.rst:2855 +#: ../../api.rst:2873 +#: ../../api.rst:2891 msgid "When this is the action, :attr:`AuditLogEntry.changes` is empty." msgstr "これが上記のactionなら、 :attr:`~AuditLogEntry.changes` は空になります。" -#: ../../api.rst:2768 +#: ../../api.rst:2861 msgid "An automod rule flagged a message." msgstr "自動管理ルールによる送信されたメッセージのフラグ付け。" -#: ../../api.rst:2786 +#: ../../api.rst:2879 msgid "An automod rule timed-out a member." msgstr "自動管理ルールによるメンバーのタイムアウト。" -#: ../../api.rst:2804 +#: ../../api.rst:2897 +msgid "A request to monetize the server was created." +msgstr "" + +#: ../../api.rst:2903 +msgid "The terms and conditions for creator monetization were accepted." +msgstr "" + +#: ../../api.rst:2909 msgid "Represents the category that the :class:`AuditLogAction` belongs to." msgstr ":class:`AuditLogAction` が属するカテゴリ。" -#: ../../api.rst:2806 +#: ../../api.rst:2911 msgid "This can be retrieved via :attr:`AuditLogEntry.category`." msgstr "これは :attr:`AuditLogEntry.category` で取得できます。" -#: ../../api.rst:2810 +#: ../../api.rst:2915 msgid "The action is the creation of something." msgstr "アクションは何かの作成です。" -#: ../../api.rst:2814 +#: ../../api.rst:2919 msgid "The action is the deletion of something." msgstr "アクションは何かの削除です。" -#: ../../api.rst:2818 +#: ../../api.rst:2923 msgid "The action is the update of something." msgstr "アクションは何かの更新です。" -#: ../../api.rst:2822 +#: ../../api.rst:2927 msgid "Represents the membership state of a team member retrieved through :func:`Client.application_info`." msgstr ":func:`Client.application_info` で取得したチームメンバーのメンバーシップ状態。" -#: ../../api.rst:2828 +#: ../../api.rst:2933 msgid "Represents an invited member." msgstr "招待されたメンバー。" -#: ../../api.rst:2832 +#: ../../api.rst:2937 msgid "Represents a member currently in the team." msgstr "現在チームにいるメンバー。" -#: ../../api.rst:2836 +#: ../../api.rst:2941 +msgid "Represents the type of role of a team member retrieved through :func:`Client.application_info`." +msgstr "" + +#: ../../api.rst:2947 +msgid "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." +msgstr "" + +#: ../../api.rst:2952 +msgid "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." +msgstr "" + +#: ../../api.rst:2958 +msgid "The team member is a read-only member. This allows them to access information, but not edit anything." +msgstr "" + +#: ../../api.rst:2962 msgid "Represents the type of webhook that can be received." msgstr "受け取れるWebhookの種類。" -#: ../../api.rst:2842 +#: ../../api.rst:2968 msgid "Represents a webhook that can post messages to channels with a token." msgstr "トークンでチャンネルにメッセージを投稿できるWebhook。" -#: ../../api.rst:2846 +#: ../../api.rst:2972 msgid "Represents a webhook that is internally managed by Discord, used for following channels." msgstr "チャンネルのフォローのためDiscord内部で管理されるWebhook。" -#: ../../api.rst:2850 +#: ../../api.rst:2976 msgid "Represents a webhook that is used for interactions or applications." msgstr "インタラクションやアプリケーションに用いられるWebhook。" -#: ../../api.rst:2856 +#: ../../api.rst:2982 msgid "Represents the behaviour the :class:`Integration` should perform when a user's subscription has finished." msgstr "ユーザーのサブスクリプションが終了した後の :class:`Integration` の動作。" -#: ../../api.rst:2859 +#: ../../api.rst:2985 msgid "There is an alias for this called ``ExpireBehavior``." msgstr "``ExpireBehavior`` という名のエイリアスがあります。" -#: ../../api.rst:2865 +#: ../../api.rst:2991 msgid "This will remove the :attr:`StreamIntegration.role` from the user when their subscription is finished." msgstr "サブスクリプションが終了したユーザーから :attr:`StreamIntegration.role` を除去します。" -#: ../../api.rst:2870 +#: ../../api.rst:2996 msgid "This will kick the user when their subscription is finished." msgstr "サブスクリプションが終了したユーザーをキックします。" -#: ../../api.rst:2874 +#: ../../api.rst:3000 msgid "Represents the default avatar of a Discord :class:`User`" msgstr "Discord :class:`User` のデフォルトのアバター。" -#: ../../api.rst:2878 +#: ../../api.rst:3004 msgid "Represents the default avatar with the colour blurple. See also :attr:`Colour.blurple`" msgstr "ブループル色のデフォルトのアバター。 :attr:`Colour.blurple` も参照してください。" -#: ../../api.rst:2882 +#: ../../api.rst:3008 msgid "Represents the default avatar with the colour grey. See also :attr:`Colour.greyple`" msgstr "灰色のデフォルトのアバター。 :attr:`Colour.greyple` も参照してください。" -#: ../../api.rst:2886 +#: ../../api.rst:3012 msgid "An alias for :attr:`grey`." msgstr ":attr:`grey` のエイリアス。" -#: ../../api.rst:2889 +#: ../../api.rst:3015 msgid "Represents the default avatar with the colour green. See also :attr:`Colour.green`" msgstr "緑色のデフォルトのアバター。 :attr:`Colour.green` も参照してください。" -#: ../../api.rst:2893 +#: ../../api.rst:3019 msgid "Represents the default avatar with the colour orange. See also :attr:`Colour.orange`" msgstr "オレンジ色のデフォルトのアバター。 :attr:`Colour.orange` も参照してください。" -#: ../../api.rst:2897 +#: ../../api.rst:3023 msgid "Represents the default avatar with the colour red. See also :attr:`Colour.red`" msgstr "赤色のデフォルトのアバター。 :attr:`Colour.red` も参照してください。" -#: ../../api.rst:2901 +#: ../../api.rst:3027 msgid "Represents the default avatar with the colour pink. See also :attr:`Colour.pink`" msgstr "ピンク色のデフォルトのアバター。 :attr:`Colour.pink` も参照してください。" -#: ../../api.rst:2908 +#: ../../api.rst:3034 msgid "Represents the type of sticker." msgstr "スタンプの種類。" -#: ../../api.rst:2914 +#: ../../api.rst:3040 msgid "Represents a standard sticker that all Nitro users can use." msgstr "Nitroユーザー全員が使用できる標準スタンプ。" -#: ../../api.rst:2918 +#: ../../api.rst:3044 msgid "Represents a custom sticker created in a guild." msgstr "ギルドで作成されたカスタムスタンプ。" -#: ../../api.rst:2922 +#: ../../api.rst:3048 msgid "Represents the type of sticker images." msgstr "スタンプ画像の種類。" -#: ../../api.rst:2928 +#: ../../api.rst:3054 msgid "Represents a sticker with a png image." msgstr "PNG画像のスタンプ。" -#: ../../api.rst:2932 +#: ../../api.rst:3058 msgid "Represents a sticker with an apng image." msgstr "APNG画像のスタンプ。" -#: ../../api.rst:2936 +#: ../../api.rst:3062 msgid "Represents a sticker with a lottie image." msgstr "ロッティー画像のスタンプ。" -#: ../../api.rst:2940 +#: ../../api.rst:3066 msgid "Represents a sticker with a gif image." msgstr "GIF画像のスタンプ。" -#: ../../api.rst:2946 +#: ../../api.rst:3072 msgid "Represents the invite type for voice channel invites." msgstr "ボイスチャンネル招待の招待タイプ。" -#: ../../api.rst:2952 +#: ../../api.rst:3078 msgid "The invite doesn't target anyone or anything." msgstr "招待の対象がないもの。" -#: ../../api.rst:2956 +#: ../../api.rst:3082 msgid "A stream invite that targets a user." msgstr "ユーザーを対象とするもの。" -#: ../../api.rst:2960 +#: ../../api.rst:3086 msgid "A stream invite that targets an embedded application." msgstr "埋め込まれたアプリケーションを対象とするもの。" -#: ../../api.rst:2964 +#: ../../api.rst:3090 msgid "Represents the camera video quality mode for voice channel participants." msgstr "ボイスチャンネル参加者のカメラビデオの画質モード。" -#: ../../api.rst:2970 +#: ../../api.rst:3096 msgid "Represents auto camera video quality." msgstr "自動のカメラビデオ画質。" -#: ../../api.rst:2974 +#: ../../api.rst:3100 msgid "Represents full camera video quality." msgstr "フルのカメラビデオ画質。" -#: ../../api.rst:2978 +#: ../../api.rst:3104 msgid "Represents the privacy level of a stage instance or scheduled event." msgstr "ステージインスタンスやスケジュールイベントのプライバシーレベル。" -#: ../../api.rst:2984 +#: ../../api.rst:3110 msgid "The stage instance or scheduled event is only accessible within the guild." msgstr "ステージインスタンスやスケジュールイベントはギルド内でのみアクセスできます。" -#: ../../api.rst:2988 +#: ../../api.rst:3114 msgid "Represents the NSFW level of a guild." msgstr "ギルドの年齢制限レベル。" -#: ../../api.rst:2996 +#: ../../api.rst:3122 msgid "Checks if two NSFW levels are equal." msgstr "二つの年齢制限レベルが等しいかを比較します。" -#: ../../api.rst:2999 +#: ../../api.rst:3125 msgid "Checks if two NSFW levels are not equal." msgstr "二つの年齢制限レベルが等しくないかを比較します。" -#: ../../api.rst:3002 +#: ../../api.rst:3128 msgid "Checks if a NSFW level is higher than another." msgstr "年齢制限レベルがあるレベルより高いか確認します。" -#: ../../api.rst:3005 +#: ../../api.rst:3131 msgid "Checks if a NSFW level is lower than another." msgstr "年齢制限レベルがあるレベルより低いか確認します。" -#: ../../api.rst:3008 +#: ../../api.rst:3134 msgid "Checks if a NSFW level is higher or equal to another." msgstr "年齢制限レベルがあるレベルと同じ、又は高いか確認します。" -#: ../../api.rst:3011 +#: ../../api.rst:3137 msgid "Checks if a NSFW level is lower or equal to another." msgstr "年齢制限レベルがあるレベルと同じ、又は低いか確認します。" -#: ../../api.rst:3015 +#: ../../api.rst:3141 msgid "The guild has not been categorised yet." msgstr "未分類のギルド。" -#: ../../api.rst:3019 +#: ../../api.rst:3145 msgid "The guild contains NSFW content." msgstr "年齢制限されたコンテンツを含むギルド。" -#: ../../api.rst:3023 +#: ../../api.rst:3149 msgid "The guild does not contain any NSFW content." msgstr "年齢制限されたコンテンツを一切含まないギルド。" -#: ../../api.rst:3027 +#: ../../api.rst:3153 msgid "The guild may contain NSFW content." msgstr "年齢制限されたコンテンツを含む可能性のあるギルド。" -#: ../../api.rst:3031 +#: ../../api.rst:3157 msgid "Supported locales by Discord. Mainly used for application command localisation." msgstr "Discordでサポートされているロケール。主にアプリケーションコマンドの多言語化に使用されます。" -#: ../../api.rst:3037 +#: ../../api.rst:3163 msgid "The ``en-US`` locale." msgstr "``en-US`` ロケール。" -#: ../../api.rst:3041 +#: ../../api.rst:3167 msgid "The ``en-GB`` locale." msgstr "``en-GB`` ロケール。" -#: ../../api.rst:3045 +#: ../../api.rst:3171 msgid "The ``bg`` locale." msgstr "``bg`` ロケール。" -#: ../../api.rst:3049 +#: ../../api.rst:3175 msgid "The ``zh-CN`` locale." msgstr "``zh-CN`` ロケール。" -#: ../../api.rst:3053 +#: ../../api.rst:3179 msgid "The ``zh-TW`` locale." msgstr "``zh-TW`` ロケール。" -#: ../../api.rst:3057 +#: ../../api.rst:3183 msgid "The ``hr`` locale." msgstr "``hr`` ロケール。" -#: ../../api.rst:3061 +#: ../../api.rst:3187 msgid "The ``cs`` locale." msgstr "``cs`` ロケール。" -#: ../../api.rst:3065 +#: ../../api.rst:3191 msgid "The ``id`` locale." msgstr "``id`` ロケール。" -#: ../../api.rst:3071 +#: ../../api.rst:3197 msgid "The ``da`` locale." msgstr "``da`` ロケール。" -#: ../../api.rst:3075 +#: ../../api.rst:3201 msgid "The ``nl`` locale." msgstr "``nl`` ロケール。" -#: ../../api.rst:3079 +#: ../../api.rst:3205 msgid "The ``fi`` locale." msgstr "``fi`` ロケール。" -#: ../../api.rst:3083 +#: ../../api.rst:3209 msgid "The ``fr`` locale." msgstr "``fr`` ロケール。" -#: ../../api.rst:3087 +#: ../../api.rst:3213 msgid "The ``de`` locale." msgstr "``de`` ロケール。" -#: ../../api.rst:3091 +#: ../../api.rst:3217 msgid "The ``el`` locale." msgstr "``el`` ロケール。" -#: ../../api.rst:3095 +#: ../../api.rst:3221 msgid "The ``hi`` locale." msgstr "``hi`` ロケール。" -#: ../../api.rst:3099 +#: ../../api.rst:3225 msgid "The ``hu`` locale." msgstr "``hu`` ロケール。" -#: ../../api.rst:3103 +#: ../../api.rst:3229 msgid "The ``it`` locale." msgstr "``it`` ロケール。" -#: ../../api.rst:3107 +#: ../../api.rst:3233 msgid "The ``ja`` locale." msgstr "``ja`` ロケール。" -#: ../../api.rst:3111 +#: ../../api.rst:3237 msgid "The ``ko`` locale." msgstr "``ko`` ロケール。" -#: ../../api.rst:3115 +#: ../../api.rst:3241 +msgid "The ``es-419`` locale." +msgstr "" + +#: ../../api.rst:3247 msgid "The ``lt`` locale." msgstr "``lt`` ロケール。" -#: ../../api.rst:3119 +#: ../../api.rst:3251 msgid "The ``no`` locale." msgstr "``no`` ロケール。" -#: ../../api.rst:3123 +#: ../../api.rst:3255 msgid "The ``pl`` locale." msgstr "``pl`` ロケール。" -#: ../../api.rst:3127 +#: ../../api.rst:3259 msgid "The ``pt-BR`` locale." msgstr "``pt-BR`` ロケール。" -#: ../../api.rst:3131 +#: ../../api.rst:3263 msgid "The ``ro`` locale." msgstr "``ro`` ロケール。" -#: ../../api.rst:3135 +#: ../../api.rst:3267 msgid "The ``ru`` locale." msgstr "``ru`` ロケール。" -#: ../../api.rst:3139 +#: ../../api.rst:3271 msgid "The ``es-ES`` locale." msgstr "``es-ES`` ロケール。" -#: ../../api.rst:3143 +#: ../../api.rst:3275 msgid "The ``sv-SE`` locale." msgstr "``sv-SE`` ロケール。" -#: ../../api.rst:3147 +#: ../../api.rst:3279 msgid "The ``th`` locale." msgstr "``th`` ロケール。" -#: ../../api.rst:3151 +#: ../../api.rst:3283 msgid "The ``tr`` locale." msgstr "``tr`` ロケール。" -#: ../../api.rst:3155 +#: ../../api.rst:3287 msgid "The ``uk`` locale." msgstr "``uk`` ロケール。" -#: ../../api.rst:3159 +#: ../../api.rst:3291 msgid "The ``vi`` locale." msgstr "``vi`` ロケール。" -#: ../../api.rst:3164 +#: ../../api.rst:3296 msgid "Represents the Multi-Factor Authentication requirement level of a guild." msgstr "ギルドの多要素認証要件レベル。" -#: ../../api.rst:3172 +#: ../../api.rst:3304 msgid "Checks if two MFA levels are equal." msgstr "二つのMFAレベルが等しいかを比較します。" -#: ../../api.rst:3175 +#: ../../api.rst:3307 msgid "Checks if two MFA levels are not equal." msgstr "二つのMFAレベルが等しくないかを比較します。" -#: ../../api.rst:3178 +#: ../../api.rst:3310 msgid "Checks if a MFA level is higher than another." msgstr "多要素認証レベルがあるレベルより厳しいか確認します。" -#: ../../api.rst:3181 +#: ../../api.rst:3313 msgid "Checks if a MFA level is lower than another." msgstr "多要素認証レベルがあるレベルより緩いか確認します。" -#: ../../api.rst:3184 +#: ../../api.rst:3316 msgid "Checks if a MFA level is higher or equal to another." msgstr "多要素認証レベルがあるレベルと同じ、又は厳しいか確認します。" -#: ../../api.rst:3187 +#: ../../api.rst:3319 msgid "Checks if a MFA level is lower or equal to another." msgstr "多要素認証レベルがあるレベルと同じ、又は緩いか確認します。" -#: ../../api.rst:3191 +#: ../../api.rst:3323 msgid "The guild has no MFA requirement." msgstr "多要素認証要件がないギルド。" -#: ../../api.rst:3195 +#: ../../api.rst:3327 msgid "The guild requires 2 factor authentication." msgstr "二要素認証を必須とするギルド。" -#: ../../api.rst:3199 +#: ../../api.rst:3331 msgid "Represents the type of entity that a scheduled event is for." msgstr "スケジュールイベントの開催場所の種類。" -#: ../../api.rst:3205 +#: ../../api.rst:3337 msgid "The scheduled event will occur in a stage instance." msgstr "ステージインスタンスで起こるスケジュールイベント。" -#: ../../api.rst:3209 +#: ../../api.rst:3341 msgid "The scheduled event will occur in a voice channel." msgstr "ボイスチャンネルで起こるスケジュールイベント。" -#: ../../api.rst:3213 +#: ../../api.rst:3345 msgid "The scheduled event will occur externally." msgstr "外部で起こるスケジュールイベント。" -#: ../../api.rst:3217 +#: ../../api.rst:3349 msgid "Represents the status of an event." msgstr "イベントの状態。" -#: ../../api.rst:3223 +#: ../../api.rst:3355 msgid "The event is scheduled." msgstr "予定されたイベント。" -#: ../../api.rst:3227 +#: ../../api.rst:3359 msgid "The event is active." msgstr "開催中のイベント。" -#: ../../api.rst:3231 +#: ../../api.rst:3363 msgid "The event has ended." msgstr "終了したイベント。" -#: ../../api.rst:3235 +#: ../../api.rst:3367 msgid "The event has been cancelled." msgstr "キャンセルされたイベント。" -#: ../../api.rst:3239 +#: ../../api.rst:3371 msgid "An alias for :attr:`cancelled`." msgstr ":attr:`cancelled` のエイリアス。" -#: ../../api.rst:3243 +#: ../../api.rst:3375 msgid "An alias for :attr:`completed`." msgstr ":attr:`completed` のエイリアス。" -#: ../../api.rst:3247 +#: ../../api.rst:3379 msgid "Represents the trigger type of an automod rule." msgstr "自動管理ルールの発動条件の種類を表します。" -#: ../../api.rst:3253 +#: ../../api.rst:3385 msgid "The rule will trigger when a keyword is mentioned." msgstr "キーワードに言及したときに発動されるルール。" -#: ../../api.rst:3257 +#: ../../api.rst:3389 msgid "The rule will trigger when a harmful link is posted." msgstr "有害なリンクを投稿したときに発動されるルール。" -#: ../../api.rst:3261 +#: ../../api.rst:3393 msgid "The rule will trigger when a spam message is posted." msgstr "スパムメッセージを投稿したときに発動されるルール。" -#: ../../api.rst:3265 +#: ../../api.rst:3397 msgid "The rule will trigger when something triggers based on the set keyword preset types." msgstr "事前に定められたキーワードプリセットに基づき発動したときに発動されるルール。" -#: ../../api.rst:3269 +#: ../../api.rst:3401 msgid "The rule will trigger when combined number of role and user mentions is greater than the set limit." msgstr "ロールとユーザーのメンションの合計数が設定された制限よりも多い場合に発動されるルール。" -#: ../../api.rst:3274 +#: ../../api.rst:3406 +msgid "The rule will trigger when a user's profile contains a keyword." +msgstr "" + +#: ../../api.rst:3412 msgid "Represents the event type of an automod rule." msgstr "自動管理ルールのイベントの種類を表します。" -#: ../../api.rst:3280 +#: ../../api.rst:3418 msgid "The rule will trigger when a message is sent." msgstr "メッセージを投稿したときにルールが発動します。" -#: ../../api.rst:3284 +#: ../../api.rst:3422 +msgid "The rule will trigger when a member's profile is updated." +msgstr "" + +#: ../../api.rst:3428 msgid "Represents the action type of an automod rule." msgstr "自動管理ルールの対応の種類を表します。" -#: ../../api.rst:3290 +#: ../../api.rst:3434 msgid "The rule will block a message from being sent." msgstr "メッセージを送信できないようにします。" -#: ../../api.rst:3294 +#: ../../api.rst:3438 msgid "The rule will send an alert message to a predefined channel." msgstr "事前に指定したチャンネルに警告メッセージを送信します。" -#: ../../api.rst:3298 +#: ../../api.rst:3442 msgid "The rule will timeout a user." msgstr "ユーザーをタイムアウトします。" -#: ../../api.rst:3303 +#: ../../api.rst:3446 +msgid "Similar to :attr:`timeout`, except the user will be timed out indefinitely. This will request the user to edit it's profile." +msgstr "" + +#: ../../api.rst:3453 msgid "Represents how a forum's posts are layed out in the client." msgstr "フォーラムの投稿がクライアントでどのように配列されるかを表します。" -#: ../../api.rst:3309 +#: ../../api.rst:3459 msgid "No default has been set, so it is up to the client to know how to lay it out." msgstr "デフォルトが設定されていないので、配列方法はクライアントによります。" -#: ../../api.rst:3313 +#: ../../api.rst:3463 msgid "Displays posts as a list." msgstr "投稿を一覧として表示します。" -#: ../../api.rst:3317 +#: ../../api.rst:3467 msgid "Displays posts as a collection of tiles." msgstr "投稿をタイルの集まりとして表示します。" -#: ../../api.rst:3322 +#: ../../api.rst:3472 msgid "Represents how a forum's posts are sorted in the client." msgstr "フォーラムの投稿がクライアントでどのように並び替えられるかを表します。" -#: ../../api.rst:3328 +#: ../../api.rst:3478 msgid "Sort forum posts by activity." msgstr "最終更新日時順でフォーラム投稿を並び替えます。" -#: ../../api.rst:3332 +#: ../../api.rst:3482 msgid "Sort forum posts by creation time (from most recent to oldest)." msgstr "作成日時順 (新しいものから古いものの順) でフォーラム投稿を並び替えます。" -#: ../../api.rst:3338 +#: ../../api.rst:3486 +msgid "Represents the default value of a select menu." +msgstr "" + +#: ../../api.rst:3492 +msgid "The underlying type of the ID is a user." +msgstr "" + +#: ../../api.rst:3496 +msgid "The underlying type of the ID is a role." +msgstr "" + +#: ../../api.rst:3500 +msgid "The underlying type of the ID is a channel or thread." +msgstr "" + +#: ../../api.rst:3505 +msgid "Represents the type of a SKU." +msgstr "" + +#: ../../api.rst:3511 +msgid "The SKU is a recurring subscription." +msgstr "" + +#: ../../api.rst:3515 +msgid "The SKU is a system-generated group which is created for each :attr:`SKUType.subscription`." +msgstr "" + +#: ../../api.rst:3520 +msgid "Represents the type of an entitlement." +msgstr "" + +#: ../../api.rst:3526 +msgid "The entitlement was purchased as an app subscription." +msgstr "" + +#: ../../api.rst:3531 +msgid "Represents the type of an entitlement owner." +msgstr "" + +#: ../../api.rst:3537 +msgid "The entitlement owner is a guild." +msgstr "" + +#: ../../api.rst:3541 +msgid "The entitlement owner is a user." +msgstr "" + +#: ../../api.rst:3547 msgid "Audit Log Data" msgstr "監査ログデータ" -#: ../../api.rst:3340 +#: ../../api.rst:3549 msgid "Working with :meth:`Guild.audit_logs` is a complicated process with a lot of machinery involved. The library attempts to make it easy to use and friendly. In order to accomplish this goal, it must make use of a couple of data classes that aid in this goal." msgstr ":meth:`Guild.audit_logs` の使用は複雑なプロセスです。このライブラリーはこれを使いやすくフレンドリーにしようと試みています。この目標の達成のためにいくつかのデータクラスを使用しています。" -#: ../../api.rst:3345 +#: ../../api.rst:3554 msgid "AuditLogEntry" msgstr "AuditLogEntry" @@ -6406,406 +6877,406 @@ msgstr ":class:`AuditLogDiff`" msgid "The target's subsequent state." msgstr "対象の直後の状態。" -#: ../../api.rst:3353 +#: ../../api.rst:3562 msgid "AuditLogChanges" msgstr "AuditLogChanges" -#: ../../api.rst:3359 +#: ../../api.rst:3568 msgid "An audit log change set." msgstr "監査ログの変更のセット。" -#: ../../api.rst:3363 +#: ../../api.rst:3572 msgid "The old value. The attribute has the type of :class:`AuditLogDiff`." msgstr "以前の値。この属性は :class:`AuditLogDiff` 型です。" -#: ../../api.rst:3365 -#: ../../api.rst:3385 +#: ../../api.rst:3574 +#: ../../api.rst:3594 msgid "Depending on the :class:`AuditLogActionCategory` retrieved by :attr:`~AuditLogEntry.category`\\, the data retrieved by this attribute differs:" msgstr ":attr:`~AuditLogEntry.category` で取得される :class:`AuditLogActionCategory` によりこの属性の値が異なります:" -#: ../../api.rst:3370 -#: ../../api.rst:3390 +#: ../../api.rst:3579 +#: ../../api.rst:3599 msgid "Category" msgstr "カテゴリー" -#: ../../api.rst:3372 -#: ../../api.rst:3392 +#: ../../api.rst:3581 +#: ../../api.rst:3601 msgid ":attr:`~AuditLogActionCategory.create`" msgstr ":attr:`~AuditLogActionCategory.create`" -#: ../../api.rst:3372 +#: ../../api.rst:3581 msgid "All attributes are set to ``None``." msgstr "全ての属性は ``None`` です。" -#: ../../api.rst:3374 -#: ../../api.rst:3394 +#: ../../api.rst:3583 +#: ../../api.rst:3603 msgid ":attr:`~AuditLogActionCategory.delete`" msgstr ":attr:`~AuditLogActionCategory.delete`" -#: ../../api.rst:3374 +#: ../../api.rst:3583 msgid "All attributes are set the value before deletion." msgstr "全ての属性は削除前の値に設定されています。" -#: ../../api.rst:3376 -#: ../../api.rst:3396 +#: ../../api.rst:3585 +#: ../../api.rst:3605 msgid ":attr:`~AuditLogActionCategory.update`" msgstr ":attr:`~AuditLogActionCategory.update`" -#: ../../api.rst:3376 +#: ../../api.rst:3585 msgid "All attributes are set the value before updating." msgstr "全ての属性は更新前の値に設定されています。" -#: ../../api.rst:3378 -#: ../../api.rst:3398 +#: ../../api.rst:3587 +#: ../../api.rst:3607 msgid "``None``" msgstr "``None``" -#: ../../api.rst:3378 -#: ../../api.rst:3398 +#: ../../api.rst:3587 +#: ../../api.rst:3607 msgid "No attributes are set." msgstr "属性が設定されていません。" -#: ../../api.rst:3383 +#: ../../api.rst:3592 msgid "The new value. The attribute has the type of :class:`AuditLogDiff`." msgstr "新しい値。この属性は :class:`AuditLogDiff` 型です。" -#: ../../api.rst:3392 +#: ../../api.rst:3601 msgid "All attributes are set to the created value" msgstr "全ての属性は作成時の値に設定されています。" -#: ../../api.rst:3394 +#: ../../api.rst:3603 msgid "All attributes are set to ``None``" msgstr "全ての属性は ``None`` です。" -#: ../../api.rst:3396 +#: ../../api.rst:3605 msgid "All attributes are set the value after updating." msgstr "全ての属性は更新後の値に設定されています。" -#: ../../api.rst:3402 +#: ../../api.rst:3611 msgid "AuditLogDiff" msgstr "AuditLogDiff" -#: ../../api.rst:3408 +#: ../../api.rst:3617 msgid "Represents an audit log \"change\" object. A change object has dynamic attributes that depend on the type of action being done. Certain actions map to certain attributes being set." msgstr "監査ログの「変更」オブジェクト。変更オブジェクトには、行われたアクションの種類によって異なる属性があります。特定のアクションが行われた場合に特定の属性が設定されます。" -#: ../../api.rst:3412 +#: ../../api.rst:3621 msgid "Note that accessing an attribute that does not match the specified action will lead to an attribute error." msgstr "指定されたアクションに一致しない属性にアクセスすると、AttributeErrorが発生することに注意してください。" -#: ../../api.rst:3415 +#: ../../api.rst:3624 msgid "To get a list of attributes that have been set, you can iterate over them. To see a list of all possible attributes that could be set based on the action being done, check the documentation for :class:`AuditLogAction`, otherwise check the documentation below for all attributes that are possible." msgstr "設定された属性のリストを取得するには、イテレートすることができます。 行われたアクションに対応した可能な属性の一覧は、 :class:`AuditLogAction` の説明を確認してください。あるいは、可能なすべての属性について、以下の説明を確認してください。" -#: ../../api.rst:3424 +#: ../../api.rst:3633 msgid "Returns an iterator over (attribute, value) tuple of this diff." msgstr "差分の(属性、値)タプルのイテレーターを返します。" -#: ../../api.rst:3428 +#: ../../api.rst:3637 msgid "A name of something." msgstr "何かの名前。" -#: ../../api.rst:3434 +#: ../../api.rst:3643 msgid "The guild of something." msgstr "ギルド属性。" -#: ../../api.rst:3440 +#: ../../api.rst:3649 msgid "A guild's or role's icon. See also :attr:`Guild.icon` or :attr:`Role.icon`." msgstr "ギルドまたはロールのアイコン。 :attr:`Guild.icon` と :attr:`Role.icon` も参照してください。" -#: ../../api.rst:3446 +#: ../../api.rst:3655 msgid "The guild's invite splash. See also :attr:`Guild.splash`." msgstr "ギルドの招待のスプラッシュ。 :attr:`Guild.splash` も参照してください。" -#: ../../api.rst:3452 +#: ../../api.rst:3661 msgid "The guild's discovery splash. See also :attr:`Guild.discovery_splash`." msgstr "ギルドのディスカバリースプラッシュ。 :attr:`Guild.discovery_splash` も参照してください。" -#: ../../api.rst:3458 +#: ../../api.rst:3667 msgid "The guild's banner. See also :attr:`Guild.banner`." msgstr "ギルドのバナー。 :attr:`Guild.banner` も参照してください。" -#: ../../api.rst:3464 +#: ../../api.rst:3673 msgid "The guild's owner. See also :attr:`Guild.owner`" msgstr "ギルドの所有者。 :attr:`Guild.owner` も参照してください。" -#: ../../api.rst:3466 +#: ../../api.rst:3675 msgid "Union[:class:`Member`, :class:`User`]" msgstr "Union[:class:`Member`, :class:`User`]" -#: ../../api.rst:3470 +#: ../../api.rst:3679 msgid "The guild's AFK channel." msgstr "ギルドのAFKチャンネル。" -#: ../../api.rst:3472 -#: ../../api.rst:3483 +#: ../../api.rst:3681 +#: ../../api.rst:3692 msgid "If this could not be found, then it falls back to a :class:`Object` with the ID being set." msgstr "見つからない場合は、IDが設定された :class:`Object` になります。" -#: ../../api.rst:3475 +#: ../../api.rst:3684 msgid "See :attr:`Guild.afk_channel`." msgstr ":attr:`Guild.afk_channel` を参照してください。" -#: ../../api.rst:3477 +#: ../../api.rst:3686 msgid "Union[:class:`VoiceChannel`, :class:`Object`]" msgstr "Union[:class:`VoiceChannel`, :class:`Object`]" -#: ../../api.rst:3481 +#: ../../api.rst:3690 msgid "The guild's system channel." msgstr "ギルドのシステムチャンネル。" -#: ../../api.rst:3486 +#: ../../api.rst:3695 msgid "See :attr:`Guild.system_channel`." msgstr ":attr:`Guild.system_channel` を参照してください。" -#: ../../api.rst:3488 -#: ../../api.rst:3500 -#: ../../api.rst:3512 -#: ../../api.rst:3539 +#: ../../api.rst:3697 +#: ../../api.rst:3709 +#: ../../api.rst:3721 +#: ../../api.rst:3748 msgid "Union[:class:`TextChannel`, :class:`Object`]" msgstr "Union[:class:`TextChannel`, :class:`Object`]" -#: ../../api.rst:3493 +#: ../../api.rst:3702 msgid "The guild's rules channel." msgstr "ギルドのルールチャンネル。" -#: ../../api.rst:3495 -#: ../../api.rst:3507 -#: ../../api.rst:3536 +#: ../../api.rst:3704 +#: ../../api.rst:3716 +#: ../../api.rst:3745 msgid "If this could not be found then it falls back to a :class:`Object` with the ID being set." msgstr "見つからない場合は、IDが設定された :class:`Object` になります。" -#: ../../api.rst:3498 +#: ../../api.rst:3707 msgid "See :attr:`Guild.rules_channel`." msgstr ":attr:`Guild.rules_channel` を参照してください。" -#: ../../api.rst:3505 +#: ../../api.rst:3714 msgid "The guild's public updates channel." msgstr "ギルドのパブリックアップデートチャンネル。" -#: ../../api.rst:3510 +#: ../../api.rst:3719 msgid "See :attr:`Guild.public_updates_channel`." msgstr ":attr:`Guild.public_updates_channel` を参照してください。" -#: ../../api.rst:3516 +#: ../../api.rst:3725 msgid "The guild's AFK timeout. See :attr:`Guild.afk_timeout`." msgstr "ギルドのAFKタイムアウト。 :attr:`Guild.afk_timeout` も参照してください。" -#: ../../api.rst:3522 +#: ../../api.rst:3731 msgid "The guild's MFA level. See :attr:`Guild.mfa_level`." msgstr "ギルドの多要素認証レベル。 :attr:`Guild.mfa_level` も参照してください。" -#: ../../api.rst:3524 +#: ../../api.rst:3733 #: ../../../discord/guild.py:docstring of discord.guild.Guild:173 msgid ":class:`MFALevel`" msgstr ":class:`MFALevel`" -#: ../../api.rst:3528 +#: ../../api.rst:3737 msgid "The guild's widget has been enabled or disabled." msgstr "ギルドのウィジェットが有効化または無効化された。" -#: ../../api.rst:3534 +#: ../../api.rst:3743 msgid "The widget's channel." msgstr "ウィジェットのチャンネル。" -#: ../../api.rst:3543 +#: ../../api.rst:3752 #: ../../../discord/guild.py:docstring of discord.guild.Guild:103 msgid "The guild's verification level." msgstr "ギルドの認証レベル。" -#: ../../api.rst:3545 +#: ../../api.rst:3754 msgid "See also :attr:`Guild.verification_level`." msgstr ":attr:`Guild.verification_level` も参照してください。" -#: ../../api.rst:3547 +#: ../../api.rst:3756 #: ../../../discord/guild.py:docstring of discord.guild.Guild:105 #: ../../../discord/invite.py:docstring of discord.invite.PartialInviteGuild:40 msgid ":class:`VerificationLevel`" msgstr ":class:`VerificationLevel`" -#: ../../api.rst:3551 +#: ../../api.rst:3760 msgid "The guild's default notification level." msgstr "ギルドのデフォルト通知レベル。" -#: ../../api.rst:3553 +#: ../../api.rst:3762 msgid "See also :attr:`Guild.default_notifications`." msgstr ":attr:`Guild.default_notifications` も参照してください。" -#: ../../api.rst:3555 +#: ../../api.rst:3764 #: ../../../discord/guild.py:docstring of discord.guild.Guild:125 msgid ":class:`NotificationLevel`" msgstr ":class:`NotificationLevel`" -#: ../../api.rst:3559 +#: ../../api.rst:3768 msgid "The guild's content filter." msgstr "ギルドのコンテンツフィルター。" -#: ../../api.rst:3561 +#: ../../api.rst:3770 msgid "See also :attr:`Guild.explicit_content_filter`." msgstr ":attr:`Guild.explicit_content_filter` も参照してください。" -#: ../../api.rst:3563 +#: ../../api.rst:3772 #: ../../../discord/guild.py:docstring of discord.guild.Guild:119 msgid ":class:`ContentFilter`" msgstr ":class:`ContentFilter`" -#: ../../api.rst:3567 +#: ../../api.rst:3776 msgid "The guild's vanity URL." msgstr "ギルドのバニティURL。" -#: ../../api.rst:3569 +#: ../../api.rst:3778 msgid "See also :meth:`Guild.vanity_invite` and :meth:`Guild.edit`." msgstr ":meth:`Guild.vanity_invite` と :meth:`Guild.edit` も参照してください。" -#: ../../api.rst:3575 +#: ../../api.rst:3784 msgid "The position of a :class:`Role` or :class:`abc.GuildChannel`." msgstr ":class:`Role` や :class:`abc.GuildChannel` の位置。" -#: ../../api.rst:3581 +#: ../../api.rst:3790 msgid "The type of channel, sticker, webhook or integration." msgstr "チャンネル、スタンプ、Webhookまたは連携サービスのタイプ。" -#: ../../api.rst:3583 +#: ../../api.rst:3792 msgid "Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`]" msgstr "Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`]" -#: ../../api.rst:3587 +#: ../../api.rst:3796 msgid "The topic of a :class:`TextChannel` or :class:`StageChannel`." msgstr ":class:`TextChannel` または :class:`StageChannel` のトピック。" -#: ../../api.rst:3589 +#: ../../api.rst:3798 msgid "See also :attr:`TextChannel.topic` or :attr:`StageChannel.topic`." msgstr ":attr:`TextChannel.topic` または :attr:`StageChannel.topic` も参照してください。" -#: ../../api.rst:3595 +#: ../../api.rst:3804 msgid "The bitrate of a :class:`VoiceChannel`." msgstr ":class:`VoiceChannel` のビットレート。" -#: ../../api.rst:3597 +#: ../../api.rst:3806 msgid "See also :attr:`VoiceChannel.bitrate`." msgstr ":attr:`VoiceChannel.bitrate` も参照してください。" -#: ../../api.rst:3603 +#: ../../api.rst:3812 msgid "A list of permission overwrite tuples that represents a target and a :class:`PermissionOverwrite` for said target." msgstr "対象とその :class:`PermissionOverwrite` のタプルで示された権限の上書きのリスト。" -#: ../../api.rst:3606 +#: ../../api.rst:3815 msgid "The first element is the object being targeted, which can either be a :class:`Member` or :class:`User` or :class:`Role`. If this object is not found then it is a :class:`Object` with an ID being filled and a ``type`` attribute set to either ``'role'`` or ``'member'`` to help decide what type of ID it is." msgstr "最初の要素は対象のオブジェクトで、 :class:`Member` か :class:`User` か :class:`Role` です。このオブジェクトが見つからない場合はこれはIDが設定され、 ``type`` 属性が ``'role'`` か ``'member'`` に設定された :class:`Object` になります。" -#: ../../api.rst:3612 +#: ../../api.rst:3821 msgid "List[Tuple[target, :class:`PermissionOverwrite`]]" msgstr "List[Tuple[target, :class:`PermissionOverwrite`]]" -#: ../../api.rst:3616 +#: ../../api.rst:3825 msgid "The privacy level of the stage instance or scheduled event" msgstr "ステージインスタンスやスケジュールイベントのプライバシーレベル。" -#: ../../api.rst:3618 +#: ../../api.rst:3827 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:65 #: ../../../discord/stage_instance.py:docstring of discord.stage_instance.StageInstance:47 msgid ":class:`PrivacyLevel`" msgstr ":class:`PrivacyLevel`" -#: ../../api.rst:3622 +#: ../../api.rst:3831 msgid "A list of roles being added or removed from a member." msgstr "メンバーから追加または削除されたロールのリスト。" -#: ../../api.rst:3624 +#: ../../api.rst:3833 msgid "If a role is not found then it is a :class:`Object` with the ID and name being filled in." msgstr "ロールが見つからない場合は、IDとnameが設定された :class:`Object` になります。" -#: ../../api.rst:3627 -#: ../../api.rst:3968 +#: ../../api.rst:3836 +#: ../../api.rst:4183 msgid "List[Union[:class:`Role`, :class:`Object`]]" msgstr "List[Union[:class:`Role`, :class:`Object`]]" -#: ../../api.rst:3631 +#: ../../api.rst:3840 msgid "The nickname of a member." msgstr "メンバーのニックネーム。" -#: ../../api.rst:3633 +#: ../../api.rst:3842 msgid "See also :attr:`Member.nick`" msgstr ":attr:`Member.nick` も参照してください。" -#: ../../api.rst:3639 +#: ../../api.rst:3848 msgid "Whether the member is being server deafened." msgstr "メンバーがサーバーでスピーカーミュートされているかどうか。" -#: ../../api.rst:3641 +#: ../../api.rst:3850 msgid "See also :attr:`VoiceState.deaf`." msgstr ":attr:`VoiceState.deaf` も参照してください。" -#: ../../api.rst:3647 +#: ../../api.rst:3856 msgid "Whether the member is being server muted." msgstr "メンバーがサーバーでミュートされているかどうか。" -#: ../../api.rst:3649 +#: ../../api.rst:3858 msgid "See also :attr:`VoiceState.mute`." msgstr ":attr:`VoiceState.mute` も参照してください。" -#: ../../api.rst:3655 +#: ../../api.rst:3864 msgid "The permissions of a role." msgstr "ロールの権限。" -#: ../../api.rst:3657 +#: ../../api.rst:3866 msgid "See also :attr:`Role.permissions`." msgstr ":attr:`Role.permissions` も参照してください。" -#: ../../api.rst:3664 +#: ../../api.rst:3873 msgid "The colour of a role." msgstr "ロールの色。" -#: ../../api.rst:3666 +#: ../../api.rst:3875 msgid "See also :attr:`Role.colour`" msgstr ":attr:`Role.colour` も参照してください。" -#: ../../api.rst:3672 +#: ../../api.rst:3881 msgid "Whether the role is being hoisted or not." msgstr "役割が別に表示されるかどうか。" -#: ../../api.rst:3674 +#: ../../api.rst:3883 msgid "See also :attr:`Role.hoist`" msgstr ":attr:`Role.hoist` も参照してください。" -#: ../../api.rst:3680 +#: ../../api.rst:3889 msgid "Whether the role is mentionable or not." msgstr "役割がメンションできるかどうか。" -#: ../../api.rst:3682 +#: ../../api.rst:3891 msgid "See also :attr:`Role.mentionable`" msgstr ":attr:`Role.mentionable` も参照してください。" -#: ../../api.rst:3688 +#: ../../api.rst:3897 msgid "The invite's code." msgstr "招待のコード。" -#: ../../api.rst:3690 +#: ../../api.rst:3899 msgid "See also :attr:`Invite.code`" msgstr ":attr:`Invite.code` も参照してください。" -#: ../../api.rst:3696 +#: ../../api.rst:3905 msgid "A guild channel." msgstr "ギルドのチャンネル。" -#: ../../api.rst:3698 +#: ../../api.rst:3907 msgid "If the channel is not found then it is a :class:`Object` with the ID being set. In some cases the channel name is also set." msgstr "チャンネルが見つからない場合は、IDが設定された :class:`Object` になります。 場合によっては、チャンネル名も設定されています。" -#: ../../api.rst:3701 +#: ../../api.rst:3910 msgid "Union[:class:`abc.GuildChannel`, :class:`Object`]" msgstr "Union[:class:`abc.GuildChannel`, :class:`Object`]" -#: ../../api.rst:3705 +#: ../../api.rst:3914 #: ../../../discord/invite.py:docstring of discord.invite.Invite:101 msgid "The user who created the invite." msgstr "招待を作成したユーザー。" -#: ../../api.rst:3707 +#: ../../api.rst:3916 msgid "See also :attr:`Invite.inviter`." msgstr ":attr:`Invite.inviter` も参照してください。" -#: ../../api.rst:3709 +#: ../../api.rst:3918 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:83 #: ../../../discord/integrations.py:docstring of discord.integrations.IntegrationApplication:39 #: ../../../discord/emoji.py:docstring of discord.emoji.Emoji:76 @@ -6813,71 +7284,71 @@ msgstr ":attr:`Invite.inviter` も参照してください。" msgid "Optional[:class:`User`]" msgstr "Optional[:class:`User`]" -#: ../../api.rst:3713 +#: ../../api.rst:3922 msgid "The invite's max uses." msgstr "招待の最大使用回数。" -#: ../../api.rst:3715 +#: ../../api.rst:3924 msgid "See also :attr:`Invite.max_uses`." msgstr ":attr:`Invite.max_uses` も参照してください。" -#: ../../api.rst:3721 +#: ../../api.rst:3930 msgid "The invite's current uses." msgstr "招待の現在の使用回数。" -#: ../../api.rst:3723 +#: ../../api.rst:3932 msgid "See also :attr:`Invite.uses`." msgstr ":attr:`Invite.uses` も参照してください。" -#: ../../api.rst:3729 +#: ../../api.rst:3938 msgid "The invite's max age in seconds." msgstr "招待者の最大時間は秒数です。" -#: ../../api.rst:3731 +#: ../../api.rst:3940 msgid "See also :attr:`Invite.max_age`." msgstr ":attr:`Invite.max_age` も参照してください。" -#: ../../api.rst:3737 +#: ../../api.rst:3946 msgid "If the invite is a temporary invite." msgstr "招待が一時的な招待であるか。" -#: ../../api.rst:3739 +#: ../../api.rst:3948 msgid "See also :attr:`Invite.temporary`." msgstr ":attr:`Invite.temporary` も参照してください。" -#: ../../api.rst:3746 +#: ../../api.rst:3955 msgid "The permissions being allowed or denied." msgstr "許可または拒否された権限。" -#: ../../api.rst:3752 +#: ../../api.rst:3961 msgid "The ID of the object being changed." msgstr "変更されたオブジェクトのID。" -#: ../../api.rst:3758 +#: ../../api.rst:3967 msgid "The avatar of a member." msgstr "メンバーのアバター。" -#: ../../api.rst:3760 +#: ../../api.rst:3969 msgid "See also :attr:`User.avatar`." msgstr ":attr:`User.avatar` も参照してください。" -#: ../../api.rst:3766 +#: ../../api.rst:3975 msgid "The number of seconds members have to wait before sending another message in the channel." msgstr "メンバーが別のメッセージをチャンネルに送信するまでの秒単位の待ち時間。" -#: ../../api.rst:3769 +#: ../../api.rst:3978 msgid "See also :attr:`TextChannel.slowmode_delay`." msgstr ":attr:`TextChannel.slowmode_delay` も参照してください。" -#: ../../api.rst:3775 +#: ../../api.rst:3984 msgid "The region for the voice channel’s voice communication. A value of ``None`` indicates automatic voice region detection." msgstr "ボイスチャンネルの音声通信のためのリージョン。値が ``None`` の場合は自動で検出されます。" -#: ../../api.rst:3778 +#: ../../api.rst:3987 msgid "See also :attr:`VoiceChannel.rtc_region`." msgstr ":attr:`VoiceChannel.rtc_region` も参照してください。" -#: ../../api.rst:3784 +#: ../../api.rst:3993 #: ../../../discord/guild.py:docstring of discord.guild.Guild.create_voice_channel:31 #: ../../../discord/guild.py:docstring of discord.guild.Guild.create_stage_channel:37 #: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel:86 @@ -6885,25 +7356,25 @@ msgstr ":attr:`VoiceChannel.rtc_region` も参照してください。" msgid "The camera video quality for the voice channel's participants." msgstr "ボイスチャンネル参加者のカメラビデオの画質。" -#: ../../api.rst:3786 +#: ../../api.rst:3995 msgid "See also :attr:`VoiceChannel.video_quality_mode`." msgstr ":attr:`VoiceChannel.video_quality_mode` も参照してください。" -#: ../../api.rst:3788 +#: ../../api.rst:3997 #: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel:90 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel:93 msgid ":class:`VideoQualityMode`" msgstr ":class:`VideoQualityMode`" -#: ../../api.rst:3792 +#: ../../api.rst:4001 msgid "The format type of a sticker being changed." msgstr "変更されたスタンプのフォーマットの種類。" -#: ../../api.rst:3794 +#: ../../api.rst:4003 msgid "See also :attr:`GuildSticker.format`" msgstr ":attr:`GuildSticker.format` も参照してください。" -#: ../../api.rst:3796 +#: ../../api.rst:4005 #: ../../../discord/sticker.py:docstring of discord.sticker.StickerItem:35 #: ../../../discord/sticker.py:docstring of discord.sticker.Sticker:47 #: ../../../discord/sticker.py:docstring of discord.sticker.StandardSticker:47 @@ -6911,306 +7382,315 @@ msgstr ":attr:`GuildSticker.format` も参照してください。" msgid ":class:`StickerFormatType`" msgstr ":class:`StickerFormatType`" -#: ../../api.rst:3800 +#: ../../api.rst:4009 msgid "The name of the emoji that represents a sticker being changed." msgstr "変更されたスタンプを示す絵文字の名前。" -#: ../../api.rst:3802 +#: ../../api.rst:4011 msgid "See also :attr:`GuildSticker.emoji`." msgstr ":attr:`GuildSticker.emoji` も参照してください。" -#: ../../api.rst:3808 +#: ../../api.rst:4017 msgid "The unicode emoji that is used as an icon for the role being changed." msgstr "変更されたロールのアイコンとして使用されるUnicode絵文字。" -#: ../../api.rst:3810 +#: ../../api.rst:4019 msgid "See also :attr:`Role.unicode_emoji`." msgstr ":attr:`Role.unicode_emoji` も参照してください。" -#: ../../api.rst:3816 +#: ../../api.rst:4025 msgid "The description of a guild, a sticker, or a scheduled event." msgstr "ギルド、スタンプ、またはスケジュールイベントの説明。" -#: ../../api.rst:3818 +#: ../../api.rst:4027 msgid "See also :attr:`Guild.description`, :attr:`GuildSticker.description`, or :attr:`ScheduledEvent.description`." msgstr ":attr:`Guild.description` 、 :attr:`GuildSticker.description` 、または :attr:`ScheduledEvent.description` も参照してください。" -#: ../../api.rst:3825 +#: ../../api.rst:4034 msgid "The availability of a sticker being changed." msgstr "変更されたスタンプの利用可能かどうかの状態。" -#: ../../api.rst:3827 +#: ../../api.rst:4036 msgid "See also :attr:`GuildSticker.available`" msgstr ":attr:`GuildSticker.available` も参照してください。" -#: ../../api.rst:3833 +#: ../../api.rst:4042 msgid "The thread is now archived." msgstr "スレッドがアーカイブされたか。" -#: ../../api.rst:3839 +#: ../../api.rst:4048 msgid "The thread is being locked or unlocked." msgstr "スレッドがロックされ、またはロックが解除されたかどうか。" -#: ../../api.rst:3845 +#: ../../api.rst:4054 msgid "The thread's auto archive duration being changed." msgstr "変更されたスレッドの自動アーカイブ期間。" -#: ../../api.rst:3847 +#: ../../api.rst:4056 msgid "See also :attr:`Thread.auto_archive_duration`" msgstr ":attr:`Thread.auto_archive_duration` も参照してください。" -#: ../../api.rst:3853 +#: ../../api.rst:4062 msgid "The default auto archive duration for newly created threads being changed." msgstr "変更された新規作成されたスレッドの既定の自動アーカイブ期間。" -#: ../../api.rst:3859 +#: ../../api.rst:4068 msgid "Whether non-moderators can add users to this private thread." msgstr "モデレータ以外がプライベートスレッドにユーザーを追加できるかどうか。" -#: ../../api.rst:3865 +#: ../../api.rst:4074 msgid "Whether the user is timed out, and if so until when." msgstr "ユーザーがタイムアウトされているかどうか、そしてその場合はいつまでか。" -#: ../../api.rst:3867 +#: ../../api.rst:4076 #: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.edited_at:3 #: ../../../discord/message.py:docstring of discord.Message.edited_at:3 -#: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:59 -#: ../../../discord/member.py:docstring of discord.member.Member:30 +#: ../../../discord/guild.py:docstring of discord.Guild.invites_paused_until:6 +#: ../../../discord/guild.py:docstring of discord.Guild.dms_paused_until:6 msgid "Optional[:class:`datetime.datetime`]" msgstr "Optional[:class:`datetime.datetime`]" -#: ../../api.rst:3871 +#: ../../api.rst:4080 msgid "Integration emoticons were enabled or disabled." msgstr "連携サービスの絵文字が有効化され、または無効化されたか。" -#: ../../api.rst:3873 +#: ../../api.rst:4082 msgid "See also :attr:`StreamIntegration.enable_emoticons`" msgstr ":attr:`StreamIntegration.enable_emoticons` も参照してください。" -#: ../../api.rst:3880 +#: ../../api.rst:4089 msgid "The behaviour of expiring subscribers changed." msgstr "変更された期限切れのサブスクライバーの動作。" -#: ../../api.rst:3882 +#: ../../api.rst:4091 msgid "See also :attr:`StreamIntegration.expire_behaviour`" msgstr ":attr:`StreamIntegration.expire_behaviour` も参照してください。" -#: ../../api.rst:3884 +#: ../../api.rst:4093 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration:51 #: ../../../discord/integrations.py:docstring of discord.StreamIntegration.expire_behavior:3 msgid ":class:`ExpireBehaviour`" msgstr ":class:`ExpireBehaviour`" -#: ../../api.rst:3888 +#: ../../api.rst:4097 msgid "The grace period before expiring subscribers changed." msgstr "変更された期限切れのサブスクライバーの猶予期間。" -#: ../../api.rst:3890 +#: ../../api.rst:4099 msgid "See also :attr:`StreamIntegration.expire_grace_period`" msgstr ":attr:`StreamIntegration.expire_grace_period` も参照してください。" -#: ../../api.rst:3896 +#: ../../api.rst:4105 msgid "The preferred locale for the guild changed." msgstr "変更されたギルドの優先ローケル。" -#: ../../api.rst:3898 +#: ../../api.rst:4107 msgid "See also :attr:`Guild.preferred_locale`" msgstr ":attr:`Guild.preferred_locale` も参照してください。" -#: ../../api.rst:3900 +#: ../../api.rst:4109 #: ../../../discord/guild.py:docstring of discord.guild.Guild:156 msgid ":class:`Locale`" msgstr ":class:`Locale`" -#: ../../api.rst:3904 +#: ../../api.rst:4113 msgid "The number of days after which inactive and role-unassigned members are kicked has been changed." msgstr "変更された活動していない、かつロールが割り当てられていないメンバーがキックさえるまでの日数。" -#: ../../api.rst:3910 +#: ../../api.rst:4119 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:69 msgid "The status of the scheduled event." msgstr "スケジュールイベントのステータス。" -#: ../../api.rst:3912 +#: ../../api.rst:4121 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:71 msgid ":class:`EventStatus`" msgstr ":class:`EventStatus`" -#: ../../api.rst:3916 +#: ../../api.rst:4125 msgid "The type of entity this scheduled event is for." msgstr "スケジュールイベントの開催場所の種類。" -#: ../../api.rst:3918 +#: ../../api.rst:4127 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:41 msgid ":class:`EntityType`" msgstr ":class:`EntityType`" -#: ../../api.rst:3922 +#: ../../api.rst:4131 #: ../../../discord/scheduled_event.py:docstring of discord.ScheduledEvent.cover_image:1 msgid "The scheduled event's cover image." msgstr "スケジュールイベントのカバー画像。" -#: ../../api.rst:3924 +#: ../../api.rst:4133 msgid "See also :attr:`ScheduledEvent.cover_image`." msgstr ":attr:`ScheduledEvent.cover_image` も参照してください。" -#: ../../api.rst:3930 +#: ../../api.rst:4139 msgid "List of permissions for the app command." msgstr "アプリケーションコマンドの権限のリスト。" -#: ../../api.rst:3932 +#: ../../api.rst:4141 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawAppCommandPermissionsUpdateEvent:29 msgid "List[:class:`~discord.app_commands.AppCommandPermissions`]" msgstr "List[:class:`~discord.app_commands.AppCommandPermissions`]" -#: ../../api.rst:3936 +#: ../../api.rst:4145 msgid "Whether the automod rule is active or not." msgstr "自動管理ルールが有効かどうか。" -#: ../../api.rst:3942 +#: ../../api.rst:4151 msgid "The event type for triggering the automod rule." msgstr "自動管理ルールを発動させるイベントの種類。" -#: ../../api.rst:3944 +#: ../../api.rst:4153 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:57 msgid ":class:`AutoModRuleEventType`" msgstr ":class:`AutoModRuleEventType`" -#: ../../api.rst:3948 +#: ../../api.rst:4157 msgid "The trigger type for the automod rule." msgstr "自動管理ルールの発動条件の種類。" -#: ../../api.rst:3950 +#: ../../api.rst:4159 #: ../../../discord/automod.py:docstring of discord.automod.AutoModAction:28 -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:24 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:28 msgid ":class:`AutoModRuleTriggerType`" msgstr ":class:`AutoModRuleTriggerType`" -#: ../../api.rst:3954 +#: ../../api.rst:4163 msgid "The trigger for the automod rule." msgstr "自動管理ルールの発動条件。" -#: ../../api.rst:3956 +#: ../../api.rst:4167 +msgid "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." +msgstr "" + +#: ../../api.rst:4171 #: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:33 msgid ":class:`AutoModTrigger`" msgstr ":class:`AutoModTrigger`" -#: ../../api.rst:3960 +#: ../../api.rst:4175 msgid "The actions to take when an automod rule is triggered." msgstr "自動管理ルールの発動時の対応。" -#: ../../api.rst:3962 +#: ../../api.rst:4177 msgid "List[AutoModRuleAction]" msgstr "List[AutoModRuleAction]" -#: ../../api.rst:3966 +#: ../../api.rst:4181 msgid "The list of roles that are exempt from the automod rule." msgstr "自動管理ルールの除外対象のロールのリスト。" -#: ../../api.rst:3972 +#: ../../api.rst:4187 msgid "The list of channels or threads that are exempt from the automod rule." msgstr "自動管理ルールの除外対象のチャンネルまたはスレッドのリスト。" -#: ../../api.rst:3974 +#: ../../api.rst:4189 msgid "List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`]" msgstr "List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`]" -#: ../../api.rst:3978 +#: ../../api.rst:4193 msgid "The guild’s display setting to show boost progress bar." msgstr "ギルドのブーストの進捗バーを表示するかの設定。" -#: ../../api.rst:3984 +#: ../../api.rst:4199 msgid "The guild’s system channel settings." msgstr "ギルドのシステムチャンネルの設定。" -#: ../../api.rst:3986 +#: ../../api.rst:4201 msgid "See also :attr:`Guild.system_channel_flags`" msgstr ":attr:`Guild.system_channel_flags` を参照してください。" -#: ../../api.rst:3988 +#: ../../api.rst:4203 #: ../../../discord/guild.py:docstring of discord.Guild.system_channel_flags:3 msgid ":class:`SystemChannelFlags`" msgstr ":class:`SystemChannelFlags`" -#: ../../api.rst:3992 +#: ../../api.rst:4207 msgid "Whether the channel is marked as “not safe for work” or “age restricted”." msgstr "チャンネルに年齢制限がかかっているか。" -#: ../../api.rst:3998 +#: ../../api.rst:4213 msgid "The channel’s limit for number of members that can be in a voice or stage channel." msgstr "ボイスまたはステージチャンネルに参加できるメンバー数の制限。" -#: ../../api.rst:4000 +#: ../../api.rst:4215 msgid "See also :attr:`VoiceChannel.user_limit` and :attr:`StageChannel.user_limit`" msgstr ":attr:`VoiceChannel.user_limit` と :attr:`StageChannel.user_limit` も参照してください。" -#: ../../api.rst:4006 +#: ../../api.rst:4221 msgid "The channel flags associated with this thread or forum post." msgstr "このスレッドやフォーラム投稿に関連付けられたチャンネルフラグ。" -#: ../../api.rst:4008 +#: ../../api.rst:4223 msgid "See also :attr:`ForumChannel.flags` and :attr:`Thread.flags`" msgstr ":attr:`ForumChannel.flags` と :attr:`Thread.flags` も参照してください。" -#: ../../api.rst:4010 +#: ../../api.rst:4225 #: ../../../discord/channel.py:docstring of discord.ForumChannel.flags:5 #: ../../../discord/threads.py:docstring of discord.Thread.flags:3 msgid ":class:`ChannelFlags`" msgstr ":class:`ChannelFlags`" -#: ../../api.rst:4014 +#: ../../api.rst:4229 msgid "The default slowmode delay for threads created in this text channel or forum." msgstr "このテキストチャンネルやフォーラムで作成されたスレッドのデフォルトの低速モードのレート制限。" -#: ../../api.rst:4016 +#: ../../api.rst:4231 msgid "See also :attr:`TextChannel.default_thread_slowmode_delay` and :attr:`ForumChannel.default_thread_slowmode_delay`" msgstr ":attr:`TextChannel.default_thread_slowmode_delay` と :attr:`ForumChannel.default_thread_slowmode_delay` も参照してください。" -#: ../../api.rst:4022 +#: ../../api.rst:4237 msgid "The applied tags of a forum post." msgstr "フォーラム投稿に適用されたタグ。" -#: ../../api.rst:4024 +#: ../../api.rst:4239 msgid "See also :attr:`Thread.applied_tags`" msgstr ":attr:`Thread.applied_tags` も参照してください。" -#: ../../api.rst:4026 +#: ../../api.rst:4241 msgid "List[Union[:class:`ForumTag`, :class:`Object`]]" msgstr "List[Union[:class:`ForumTag`, :class:`Object`]]" -#: ../../api.rst:4030 +#: ../../api.rst:4245 msgid "The available tags of a forum." msgstr "フォーラムにて利用可能なタグ。" -#: ../../api.rst:4032 +#: ../../api.rst:4247 msgid "See also :attr:`ForumChannel.available_tags`" msgstr ":attr:`ForumChannel.available_tags` も参照してください。" -#: ../../api.rst:4034 +#: ../../api.rst:4249 #: ../../../discord/channel.py:docstring of discord.ForumChannel.available_tags:5 msgid "Sequence[:class:`ForumTag`]" msgstr "Sequence[:class:`ForumTag`]" -#: ../../api.rst:4038 +#: ../../api.rst:4253 msgid "The default_reaction_emoji for forum posts." msgstr "フォーラム投稿の default_reaction_emoji。" -#: ../../api.rst:4040 +#: ../../api.rst:4255 msgid "See also :attr:`ForumChannel.default_reaction_emoji`" msgstr ":attr:`ForumChannel.default_reaction_emoji` も参照してください。" -#: ../../api.rst:4042 -msgid ":class:`default_reaction_emoji`" -msgstr ":class:`default_reaction_emoji`" +#: ../../api.rst:4257 +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:105 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:100 +#: ../../../discord/activity.py:docstring of discord.activity.CustomActivity:33 +#: ../../../discord/channel.py:docstring of discord.channel.ForumTag:48 +msgid "Optional[:class:`PartialEmoji`]" +msgstr "Optional[:class:`PartialEmoji`]" -#: ../../api.rst:4048 +#: ../../api.rst:4263 msgid "Webhook Support" msgstr "Webhookサポート" -#: ../../api.rst:4050 +#: ../../api.rst:4265 msgid "discord.py offers support for creating, editing, and executing webhooks through the :class:`Webhook` class." msgstr "discord.pyは、 :class:`Webhook` クラスからWebhookの作成、編集、実行をサポートします。" -#: ../../api.rst:4053 +#: ../../api.rst:4268 msgid "Webhook" msgstr "Webhook" @@ -7666,7 +8146,11 @@ msgstr "メッセージの埋め込みを抑制するかどうか。これが `` 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 "メッセージのプッシュ通知とデスクトップ通知を抑制するかどうか。 抑制した場合、UIのメンションカウンターを増やしますが、実際に通知を送信することはありません。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:84 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:83 +msgid "Tags to apply to the thread if the webhook belongs to a :class:`~discord.ForumChannel`." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:88 #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.reply:12 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:63 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.send:82 @@ -7674,31 +8158,31 @@ msgstr "メッセージのプッシュ通知とデスクトップ通知を抑制 msgid "Sending the message failed." msgstr "メッセージの送信に失敗した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:85 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:89 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:64 msgid "This webhook was not found." msgstr "Webhookが見つからなかった場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:86 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:90 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:65 msgid "The authorization token for the webhook is incorrect." msgstr "Webhookの認証トークンが正しくない場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:87 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:91 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:66 msgid "You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` or ``thread`` and ``thread_name``." msgstr "``embed`` と ``embeds`` または ``file`` と ``files`` または ``thread`` と ``thread_name`` の両方を指定した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:88 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:92 msgid "The length of ``embeds`` was invalid, there was no token associated with this webhook or ``ephemeral`` was passed with the improper webhook type or there was no state attached with this webhook when giving it a view." msgstr "``embeds`` の長さが不正な場合、Webhookにトークンが紐づいていない場合、 ``ephemeral`` が適切でない種類のWebhookに渡された場合、またはステートが付属していないWebhookにビューを渡した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:90 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:94 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:69 msgid "If ``wait`` is ``True`` then the message that was sent, otherwise ``None``." msgstr "``wait`` が ``True`` のとき、送信されたメッセージ、それ以外の場合は ``None`` 。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:91 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:95 msgid "Optional[:class:`WebhookMessage`]" msgstr "Optional[:class:`WebhookMessage`]" @@ -7899,7 +8383,7 @@ msgstr "メッセージの削除に失敗した場合。" msgid "Deleted a message that is not yours." msgstr "自分以外のメッセージを削除しようとした場合。" -#: ../../api.rst:4062 +#: ../../api.rst:4277 msgid "WebhookMessage" msgstr "WebhookMessage" @@ -8229,7 +8713,9 @@ msgid "The created thread." msgstr "作成されたスレッド。" #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.create_thread:31 +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:17 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.create_thread:31 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:17 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.create_thread:31 msgid ":class:`.Thread`" msgstr ":class:`.Thread`" @@ -8270,6 +8756,42 @@ msgstr "完全なメッセージ。" msgid ":class:`Message`" msgstr ":class:`Message`" +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:3 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:3 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:3 +msgid "Retrieves the public thread attached to this message." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:7 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:7 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:7 +msgid "This method is an API call. For general usage, consider :attr:`thread` instead." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:11 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:11 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:11 +msgid "An unknown channel type was received from Discord or the guild the thread belongs to is not the same as the one in this object points to." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:12 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:12 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:12 +msgid "Retrieving the thread failed." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:13 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:13 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:13 +msgid "There is no thread attached to this message." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:16 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:16 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:16 +msgid "The public thread attached to this message." +msgstr "" + #: ../../../discord/webhook/async_.py:docstring of discord.message.Message.is_system:1 #: ../../../discord/message.py:docstring of discord.message.Message.is_system:1 msgid ":class:`bool`: Whether the message is a system message." @@ -8492,6 +9014,25 @@ msgstr ":attr:`Message.type` に関わらず、レンダリングされた際の msgid "In the case of :attr:`MessageType.default` and :attr:`MessageType.reply`\\, this just returns the regular :attr:`Message.content`. Otherwise this returns an English message denoting the contents of the system message." msgstr ":attr:`MessageType.default` と :attr:`MessageType.reply` の場合、これは :attr:`Message.content` と同じものを返すだけです。しかしそれ以外の場合は、システムメッセージの英語版を返します。" +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:1 +#: ../../../discord/message.py:docstring of discord.Message.thread:1 +#: ../../../discord/message.py:docstring of discord.PartialMessage.thread:1 +msgid "The public thread created from this message, if it exists." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:5 +#: ../../../discord/message.py:docstring of discord.Message.thread:5 +msgid "For messages received via the gateway this does not retrieve archived threads, as they are not retained in the internal cache. Use :meth:`fetch_thread` instead." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:10 +#: ../../../discord/message.py:docstring of discord.Message.thread:10 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.get_thread:14 +#: ../../../discord/channel.py:docstring of discord.channel.TextChannel.get_thread:14 +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.get_thread:14 +msgid "Optional[:class:`Thread`]" +msgstr "Optional[:class:`Thread`]" + #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.to_reference:1 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.to_reference:1 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.to_reference:1 @@ -8540,7 +9081,7 @@ msgstr "このメッセージのピン留めを外す権限を持っていない msgid "Unpinning the message failed." msgstr "メッセージのピン留め解除に失敗した場合。" -#: ../../api.rst:4071 +#: ../../api.rst:4286 msgid "SyncWebhook" msgstr "SyncWebhook" @@ -8593,7 +9134,7 @@ msgstr "このwebhookが送信した :class:`~discord.SyncWebhookMessage` を1 msgid ":class:`~discord.SyncWebhookMessage`" msgstr ":class:`~discord.SyncWebhookMessage`" -#: ../../api.rst:4080 +#: ../../api.rst:4295 msgid "SyncWebhookMessage" msgstr "SyncWebhookMessage" @@ -8607,19 +9148,19 @@ msgstr ":class:`SyncWebhookMessage`" msgid "If provided, the number of seconds to wait before deleting the message. This blocks the thread." msgstr "指定された場合、メッセージを削除するまでの待機秒数。これはスレッドをブロックします。" -#: ../../api.rst:4090 +#: ../../api.rst:4305 msgid "Abstract Base Classes" msgstr "抽象基底クラス" -#: ../../api.rst:4092 +#: ../../api.rst:4307 msgid "An :term:`abstract base class` (also known as an ``abc``) is a class that models can inherit to get their behaviour. **Abstract base classes should not be instantiated**. They are mainly there for usage with :func:`isinstance` and :func:`issubclass`\\." msgstr ":term:`abstract base class` ( ``abc`` という名称でも知られています)はモデルが振る舞いを得るために継承するクラスです。 **抽象基底クラスはインスタンス化されるべきではありません。** これらは主に :func:`isinstance` や :func:`issubclass` で使用するために存在します。" -#: ../../api.rst:4096 +#: ../../api.rst:4311 msgid "This library has a module related to abstract base classes, in which all the ABCs are subclasses of :class:`typing.Protocol`." msgstr "このライブラリには抽象基底クラスに関連するモジュールがあり、その中の抽象基底クラスは全て :class:`typing.Protocol` のサブクラスです。" -#: ../../api.rst:4100 +#: ../../api.rst:4315 msgid "Snowflake" msgstr "Snowflake" @@ -8639,8 +9180,8 @@ msgstr "スノーフレークを自分で作成したい場合は、 :class:`.Ob msgid "The model's unique ID." msgstr "モデルのユニークなID。" -#: ../../api.rst:4108 -#: ../../api.rst:4188 +#: ../../api.rst:4323 +#: ../../api.rst:4403 msgid "User" msgstr "User" @@ -8700,15 +9241,24 @@ msgid "Returns an Asset that represents the user's avatar, if present." msgstr "ユーザーのアバターが存在する場合は、それを表すアセットを返します。" #: ../../../discord/abc.py:docstring of discord.abc.User.avatar:3 +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration:5 msgid "Optional[:class:`~discord.Asset`]" msgstr "Optional[:class:`~discord.Asset`]" +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration:1 +msgid "Returns an Asset that represents the user's avatar decoration, if present." +msgstr "" + +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration_sku_id:1 +msgid "Returns an integer that represents the user's avatar decoration SKU ID, if present." +msgstr "" + #: ../../../discord/abc.py:docstring of discord.abc.User.default_avatar:3 #: ../../../discord/abc.py:docstring of discord.abc.User.display_avatar:7 msgid ":class:`~discord.Asset`" msgstr ":class:`~discord.Asset`" -#: ../../api.rst:4116 +#: ../../api.rst:4331 msgid "PrivateChannel" msgstr "PrivateChannel" @@ -8732,7 +9282,7 @@ msgstr ":class:`~discord.GroupChannel`" msgid "The user presenting yourself." msgstr "あなた自身を示すユーザー。" -#: ../../api.rst:4124 +#: ../../api.rst:4339 msgid "GuildChannel" msgstr "GuildChannel" @@ -9600,7 +10150,7 @@ msgstr "現時点でアクティブな招待のリスト。" msgid "List[:class:`~discord.Invite`]" msgstr "List[:class:`~discord.Invite`]" -#: ../../api.rst:4132 +#: ../../api.rst:4347 msgid "Messageable" msgstr "Messageable" @@ -9944,7 +10494,7 @@ msgstr "メッセージ履歴の取得に失敗した場合。" msgid ":class:`~discord.Message` -- The message with the message data parsed." msgstr ":class:`~discord.Message` -- メッセージデータをパースしたメッセージ。" -#: ../../api.rst:4144 +#: ../../api.rst:4359 msgid "Connectable" msgstr "Connectable" @@ -9967,8 +10517,8 @@ msgstr ":attr:`~discord.Intents.voice_states` が必要です。" #: ../../../discord/abc.py:docstring of discord.abc.Connectable.connect:8 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:8 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:8 -msgid "The timeout in seconds to wait for the voice endpoint." -msgstr "ボイスエンドポイントを待つためのタイムアウト秒数。" +msgid "The timeout in seconds to wait the connection to complete." +msgstr "" #: ../../../discord/abc.py:docstring of discord.abc.Connectable.connect:10 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:10 @@ -10012,32 +10562,32 @@ msgstr "ボイスサーバーに完全に接続されたボイスクライアン msgid ":class:`~discord.VoiceProtocol`" msgstr ":class:`~discord.VoiceProtocol`" -#: ../../api.rst:4154 +#: ../../api.rst:4369 msgid "Discord Models" msgstr "Discordモデル" -#: ../../api.rst:4156 +#: ../../api.rst:4371 msgid "Models are classes that are received from Discord and are not meant to be created by the user of the library." msgstr "モデルはDiscordから受け取るクラスであり、ユーザーによって作成されることを想定していません。" -#: ../../api.rst:4161 +#: ../../api.rst:4376 msgid "The classes listed below are **not intended to be created by users** and are also **read-only**." msgstr "下記のクラスは、 **ユーザーによって作成されることを想定しておらず** 、中には **読み取り専用** のものもあります。" -#: ../../api.rst:4164 +#: ../../api.rst:4379 msgid "For example, this means that you should not make your own :class:`User` instances nor should you modify the :class:`User` instance yourself." msgstr "つまり、独自の :class:`User` を作成したりするべきではなく、また、 :class:`User` インスタンスの値の変更もするべきではありません。" -#: ../../api.rst:4167 +#: ../../api.rst:4382 msgid "If you want to get one of these model classes instances they'd have to be through the cache, and a common way of doing so is through the :func:`utils.find` function or attributes of model classes that you receive from the events specified in the :ref:`discord-api-events`." msgstr "このようなモデルクラスのインスタンスを取得したい場合は、 キャッシュを経由して取得する必要があります。一般的な方法としては :func:`utils.find` 関数を用いるか、 :ref:`discord-api-events` の特定のイベントから受け取る方法が挙げられます。" -#: ../../api.rst:4174 -#: ../../api.rst:4750 +#: ../../api.rst:4389 +#: ../../api.rst:5000 msgid "Nearly all classes here have :ref:`py:slots` defined which means that it is impossible to have dynamic attributes to the data classes." msgstr "ほぼすべてのクラスに :ref:`py:slots` が定義されています。つまり、データクラスに動的に変数を追加することは不可能です。" -#: ../../api.rst:4179 +#: ../../api.rst:4394 msgid "ClientUser" msgstr "ClientUser" @@ -10100,31 +10650,31 @@ msgstr "現在のクライアントのプロフィールを編集します。" msgid "To upload an avatar, a :term:`py:bytes-like object` must be passed in that represents the image being uploaded. If this is done through a file then the file must be opened via ``open('some_filename', 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``." msgstr "アバターをアップロードする際には、アップロードする画像を表す :term:`py:bytes-like object` を渡す必要があります。これをファイルを介して行う場合、ファイルを ``open('some_filename', 'rb')`` で開き、 :term:`py:bytes-like object` は ``fp.read()`` で取得できます。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:12 -msgid "The only image formats supported for uploading is JPEG and PNG." -msgstr "アップロードでサポートされる画像形式はJPEGとPNGのみです。" - -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:14 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:13 msgid "The edit is no longer in-place, instead the newly edited client user is returned." msgstr "編集はクライアントユーザーを置き換えず、編集された新しいクライアントユーザーが返されるようになりました。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:21 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:20 msgid "The new username you wish to change to." msgstr "変更する際の新しいユーザー名。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:23 -msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar." -msgstr "アップロードする画像を表す :term:`py:bytes-like object` 。アバターをなしにしたい場合は ``None`` を設定することが出来ます。" +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:22 +msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP." +msgstr "" + +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:26 +msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no banner. Only image formats supported for uploading are JPEG, PNG, GIF and WEBP." +msgstr "" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:27 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:33 msgid "Editing your profile failed." msgstr "プロフィールの編集に失敗した場合。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:28 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:34 msgid "Wrong image format passed for ``avatar``." msgstr "``avatar`` に渡された画像のフォーマットが間違っている場合。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:30 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:36 msgid "The newly edited client user." msgstr "新しく編集されたクライアントユーザー。" @@ -10206,6 +10756,10 @@ msgstr "Set[:class:`int`]" msgid "The IDs of the channels that are exempt from the rule." msgstr "このルールの除外対象のチャンネルのID。" +#: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:55 +msgid "The type of event that will trigger the the rule." +msgstr "" + #: ../../../discord/automod.py:docstring of discord.AutoModRule.creator:1 msgid "The member that created this rule." msgstr "このルールを作成したメンバー。" @@ -10429,7 +10983,7 @@ msgstr "ルールの取得に失敗した場合。" msgid "The rule that was executed." msgstr "発動されたルール。" -#: ../../api.rst:4214 +#: ../../api.rst:4429 msgid "Attachment" msgstr "Attachment" @@ -10501,10 +11055,6 @@ msgstr "添付ファイルが一時的であるかどうか。" msgid "The duration of the audio file in seconds. Returns ``None`` if it's not a voice message." msgstr "音声ファイルの秒単位の長さ。ボイスメッセージでない場合 ``None`` を返します。" -#: ../../../discord/message.py:docstring of discord.message.Attachment:99 -msgid "Optional[:class:`float`]" -msgstr "Optional[:class:`float`]" - #: ../../../discord/message.py:docstring of discord.message.Attachment:103 msgid "The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message." msgstr "音声の波形(振幅)をバイト単位で返します。ボイスメッセージでない場合は ``None`` を返します。" @@ -10513,6 +11063,14 @@ msgstr "音声の波形(振幅)をバイト単位で返します。ボイス msgid "Optional[:class:`bytes`]" msgstr "Optional[:class:`bytes`]" +#: ../../../discord/message.py:docstring of discord.Attachment.flags:1 +msgid "The attachment's flags." +msgstr "" + +#: ../../../discord/message.py:docstring of discord.Attachment.flags:3 +msgid ":class:`AttachmentFlags`" +msgstr "" + #: ../../../discord/message.py:docstring of discord.message.Attachment.is_spoiler:1 msgid ":class:`bool`: Whether this attachment contains a spoiler." msgstr ":class:`bool`: この添付ファイルにスポイラーが含まれているかどうか。" @@ -10607,7 +11165,7 @@ msgstr "送信に適したファイルに変換された添付ファイル。" msgid ":class:`File`" msgstr ":class:`File`" -#: ../../api.rst:4222 +#: ../../api.rst:4437 msgid "Asset" msgstr "Asset" @@ -10805,7 +11363,7 @@ msgstr "アセットがロッティータイプのスタンプであった場合 msgid "The asset as a file suitable for sending." msgstr "送信に適したファイルとしてのアセット。" -#: ../../api.rst:4231 +#: ../../api.rst:4446 msgid "Message" msgstr "Message" @@ -11077,7 +11635,7 @@ msgstr "もし指定したなら、これはメッセージを編集したあと msgid "Tried to suppress a message without permissions or edited a message's content or embed that isn't yours." msgstr "権限なしに埋め込みを除去しようとした場合や、他人のメッセージの内容や埋め込みを編集しようとした場合。" -#: ../../api.rst:4240 +#: ../../api.rst:4455 msgid "DeletedReferencedMessage" msgstr "DeletedReferencedMessage" @@ -11101,7 +11659,7 @@ msgstr "削除された参照されたメッセージが属していたチャン msgid "The guild ID of the deleted referenced message." msgstr "削除された参照されたメッセージが属していたギルドのID。" -#: ../../api.rst:4249 +#: ../../api.rst:4464 msgid "Reaction" msgstr "Reaction" @@ -11140,8 +11698,8 @@ msgid "Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]" msgstr "Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]" #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:34 -msgid "Number of times this reaction was made" -msgstr "リアクションが付けられた回数。" +msgid "Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`." +msgstr "" #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:40 msgid "If the user sent this reaction." @@ -11151,6 +11709,18 @@ msgstr "ボットがこのリアクションを付けたか。" msgid "Message this reaction is for." msgstr "このリアクションのメッセージ。" +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:52 +msgid "If the user sent this super reaction." +msgstr "" + +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:60 +msgid "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`." +msgstr "" + +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:70 +msgid "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`." +msgstr "" + #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction.is_custom_emoji:1 msgid ":class:`bool`: If this is a custom emoji." msgstr ":class:`bool`: カスタム絵文字が使用されているかどうか。" @@ -11207,7 +11777,7 @@ msgstr "リアクションを付けたユーザーの取得に失敗した場合 msgid "Union[:class:`User`, :class:`Member`] -- The member (if retrievable) or the user that has reacted to this message. The case where it can be a :class:`Member` is in a guild message context. Sometimes it can be a :class:`User` if the member has left the guild." msgstr "Union[:class:`User`, :class:`Member`] -- リアクションを付けたメンバー(取得できる場合)かユーザー。ギルド内では :class:`Member` となります。メンバーがギルドを脱退した場合は :class:`User` になります。" -#: ../../api.rst:4257 +#: ../../api.rst:4472 msgid "Guild" msgstr "Guild" @@ -11456,6 +12026,7 @@ msgid "A list of forum channels that belongs to this guild." msgstr "このギルド内に存在するフォーラムチャンネルのリスト。" #: ../../../discord/guild.py:docstring of discord.Guild.forums:5 +#: ../../../discord/channel.py:docstring of discord.CategoryChannel.forums:5 msgid "List[:class:`ForumChannel`]" msgstr "List[:class:`ForumChannel`]" @@ -11515,12 +12086,6 @@ msgstr "内部キャッシュに保持されていないため、アーカイブ msgid "The returned thread or ``None`` if not found." msgstr "スレッド、または該当するものが見つからない場合 ``None`` が返ります。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.get_thread:14 -#: ../../../discord/channel.py:docstring of discord.channel.TextChannel.get_thread:14 -#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.get_thread:14 -msgid "Optional[:class:`Thread`]" -msgstr "Optional[:class:`Thread`]" - #: ../../../discord/guild.py:docstring of discord.guild.Guild.get_emoji:8 msgid "The returned Emoji or ``None`` if not found." msgstr "絵文字、または該当するものが見つからない場合 ``None`` が返ります。" @@ -12181,24 +12746,32 @@ msgstr "レイドプロテクションのアラートをギルドで無効にす msgid "The new channel that is used for safety alerts. This is only available to guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no safety alerts channel." msgstr "安全アラートに使用される新しいチャンネル。これは :attr:`Guild.features` に ``COMMUNITY`` を含むギルドでのみ利用できます。安全アラートチャネルがない場合は ``None`` を指定できます。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:122 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:121 +msgid "The time when invites should be enabled again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:126 +msgid "The time when direct messages should be allowed again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:132 msgid "You do not have permissions to edit the guild." msgstr "ギルドを編集する権限がない場合。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:123 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:133 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration.edit:19 msgid "Editing the guild failed." msgstr "ギルドの編集に失敗した場合。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:124 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:134 msgid "The image format passed in to ``icon`` is invalid. It must be PNG or JPG. This is also raised if you are not the owner of the guild and request an ownership transfer." msgstr "``icon`` に渡された画像形式が無効な場合。これはPNGかJPGでなくてはいけません。また、あなたがギルドの所有者でないのに、ギルドの所有権の移動を行おうとした場合にも発生します。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:125 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:135 msgid "The type passed to the ``default_notifications``, ``rules_channel``, ``public_updates_channel``, ``safety_alerts_channel`` ``verification_level``, ``explicit_content_filter``, ``system_channel_flags``, or ``mfa_level`` parameter was of the incorrect type." msgstr "``default_notifications`` 、 ``rules_channel`` 、 ``public_updates_channel`` 、 ``safety_alerts_channel`` 、 ``verification_level`` 、 ``explicit_content_filter`` 、 ``system_channel_flags`` 、 ``mfa_level`` に間違った型の値が渡されたとき。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:127 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:137 msgid "The newly updated guild. Note that this has the same limitations as mentioned in :meth:`Client.fetch_guild` and may not have full data." msgstr "新しく更新されたギルド。これは :meth:`Client.fetch_guild` に記載されているものと同じ制限があり、完全なデータを持っていない可能性があることに注意してください。" @@ -12285,6 +12858,10 @@ msgstr "``member_id`` 引数は位置専用引数となりました。" msgid "The member's ID to fetch from." msgstr "取得したいメンバーのID。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:16 +msgid "You do not have access to the guild." +msgstr "ギルドにアクセスする権限がない場合。" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:17 msgid "Fetching the member failed." msgstr "メンバーの取得に失敗した場合。" @@ -12358,10 +12935,6 @@ msgstr "このユーザ以前のBANを取得します。" msgid "Retrieve bans after this user." msgstr "このユーザ以降のBANを取得します。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:33 -msgid "Both ``after`` and ``before`` were provided, as Discord does not support this type of pagination." -msgstr "``after`` と ``before`` の両方が渡された場合。Discordはこのタイプのページネーションをサポートしていません。" - #: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:35 msgid ":class:`BanEntry` -- The ban entry of the banned user." msgstr ":class:`BanEntry` -- BANされたユーザーのBANエントリー。" @@ -12375,9 +12948,8 @@ msgid "The inactive members are denoted if they have not logged on in ``days`` n msgstr "``days`` 日間ログインせずロールを持たないメンバーが非アクティブとされます。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.prune_members:8 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:7 -msgid "You must have :attr:`~Permissions.kick_members` to do this." -msgstr "これを行うには、 :attr:`~Permissions.kick_members` が必要です。" +msgid "You must have both :attr:`~Permissions.kick_members` and :attr:`~Permissions.manage_guild` to do this." +msgstr "" #: ../../../discord/guild.py:docstring of discord.guild.Guild.prune_members:10 msgid "To check how many members you would prune without actually pruning, see the :meth:`estimate_pruned_members` function." @@ -13086,7 +13658,6 @@ msgid "The guild must have ``COMMUNITY`` in :attr:`~Guild.features`." msgstr ":attr:`~Guild.features` に ``COMMUNITY`` が含まれている必要があります。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.welcome_screen:7 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:8 #: ../../../discord/guild.py:docstring of discord.guild.Guild.vanity_invite:7 msgid "You must have :attr:`~Permissions.manage_guild` to do this.as well." msgstr "これを行うには、 :attr:`~Permissions.manage_guild` も必要です。" @@ -13113,6 +13684,10 @@ msgstr ":class:`WelcomeScreen`" msgid "A shorthand method of :attr:`WelcomeScreen.edit` without needing to fetch the welcome screen beforehand." msgstr "ようこそ画面を事前に取得せずに :attr:`WelcomeScreen.edit` を呼び出すことのできるメソッド。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:8 +msgid "You must have :attr:`~Permissions.manage_guild` to do this as well." +msgstr "" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:12 msgid "The edited welcome screen." msgstr "編集した後のようこそ画面。" @@ -13127,6 +13702,10 @@ msgstr "サーバーからユーザーをキックします。" msgid "The user must meet the :class:`abc.Snowflake` abc." msgstr "ユーザーは :class:`abc.Snowflake` 抽象基底クラスのサブクラスである必要があります。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:7 +msgid "You must have :attr:`~Permissions.kick_members` to do this." +msgstr "これを行うには、 :attr:`~Permissions.kick_members` が必要です。" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:9 msgid "The user to kick from their guild." msgstr "ギルドからキックするユーザー。" @@ -13149,10 +13728,12 @@ msgstr "ギルドからユーザーをBANします。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:7 #: ../../../discord/guild.py:docstring of discord.guild.Guild.unban:7 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:7 msgid "You must have :attr:`~Permissions.ban_members` to do this." msgstr "これを行うには、 :attr:`~Permissions.ban_members` が必要です。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:9 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:11 msgid "The user to ban from their guild." msgstr "ギルドからBANするユーザー。" @@ -13173,10 +13754,12 @@ msgid "The requested user was not found." msgstr "ユーザーが見つからなかった場合。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:29 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:20 msgid "You do not have the proper permissions to ban." msgstr "BANするのに適切な権限がない場合。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:30 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:21 msgid "Banning failed." msgstr "BANに失敗した場合。" @@ -13204,6 +13787,30 @@ msgstr "BANを解除するのに適切な権限がない場合。" msgid "Unbanning failed." msgstr "BAN解除に失敗した場合。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:3 +msgid "Bans multiple users from the guild." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:5 +msgid "The users must meet the :class:`abc.Snowflake` abc." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:13 +msgid "The number of seconds worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 604800 (7 days). Defaults to 1 day." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:17 +msgid "The reason the users got banned." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:23 +msgid "The result of the bulk ban operation." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:24 +msgid ":class:`BulkBanResult`" +msgstr "" + #: ../../../discord/guild.py:docstring of discord.Guild.vanity_url:1 msgid "The Discord vanity invite URL for this guild, if available." msgstr "存在する場合、ギルドのDiscord バニティURLを返します。" @@ -13454,19 +14061,52 @@ msgstr "自動管理ルールの作成に失敗した場合。" msgid "The automod rule that was created." msgstr "作成された自動管理ルール。" -#: ../../api.rst:4266 +#: ../../../discord/guild.py:docstring of discord.Guild.invites_paused_until:1 +msgid "If invites are paused, returns when invites will get enabled in UTC, otherwise returns None." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.Guild.dms_paused_until:1 +msgid "If DMs are paused, returns when DMs will get enabled in UTC, otherwise returns None." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.invites_paused:1 +msgid ":class:`bool`: Whether invites are paused in the guild." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.dms_paused:1 +msgid ":class:`bool`: Whether DMs are paused in the guild." +msgstr "" + +#: ../../api.rst:4481 msgid "A namedtuple which represents a ban returned from :meth:`~Guild.bans`." msgstr ":meth:`~Guild.bans` から返されたBANを表すnamedtuple。" -#: ../../api.rst:4270 +#: ../../api.rst:4485 msgid "The reason this user was banned." msgstr "ユーザーがBANされた理由。" -#: ../../api.rst:4275 +#: ../../api.rst:4490 msgid "The :class:`User` that was banned." msgstr "BANされた :class:`User` 。" -#: ../../api.rst:4281 +#: ../../api.rst:4496 +msgid "A namedtuple which represents the result returned from :meth:`~Guild.bulk_ban`." +msgstr "" + +#: ../../api.rst:4502 +msgid "The list of users that were banned. The inner :class:`Object` of the list has the :attr:`Object.type` set to :class:`User`." +msgstr "" + +#: ../../api.rst:4505 +#: ../../api.rst:4511 +msgid "List[:class:`Object`]" +msgstr "" + +#: ../../api.rst:4508 +msgid "The list of users that could not be banned. The inner :class:`Object` of the list has the :attr:`Object.type` set to :class:`User`." +msgstr "" + +#: ../../api.rst:4515 msgid "ScheduledEvent" msgstr "ScheduledEvent" @@ -13719,7 +14359,7 @@ msgstr "このイベントに購読済みのユーザー。" msgid "List[:class:`User`]" msgstr "List[:class:`User`]" -#: ../../api.rst:4290 +#: ../../api.rst:4524 msgid "Integration" msgstr "Integration" @@ -13933,7 +14573,7 @@ msgstr "不完全なギルドの連携サービス。" msgid "The id of the application this integration belongs to." msgstr "このインテグレーションが属するアプリケーションのID。" -#: ../../api.rst:4323 +#: ../../api.rst:4557 msgid "Member" msgstr "Member" @@ -14057,6 +14697,14 @@ msgstr ":attr:`User.accent_color` と同じです。" msgid "Equivalent to :attr:`User.accent_colour`" msgstr ":attr:`User.accent_colour` と同じです。" +#: ../../../discord/member.py:docstring of discord.Member.avatar_decoration:1 +msgid "Equivalent to :attr:`User.avatar_decoration`" +msgstr "" + +#: ../../../discord/member.py:docstring of discord.Member.avatar_decoration_sku_id:1 +msgid "Equivalent to :attr:`User.avatar_decoration_sku_id`" +msgstr "" + #: ../../../discord/member.py:docstring of discord.Member.raw_status:1 msgid "The member's overall status as a string value." msgstr "メンバーのステータスを文字列として返します。" @@ -14329,8 +14977,8 @@ msgstr "メンバーを編集する理由。監査ログに表示されます。 #: ../../../discord/member.py:docstring of discord.member.Member.edit:60 #: ../../../discord/member.py:docstring of discord.member.Member.request_to_speak:15 -msgid "You do not have the proper permissions to the action requested." -msgstr "リクエストされたアクションをするための適切な権限がない場合。" +msgid "You do not have the proper permissions to do the action requested." +msgstr "" #: ../../../discord/member.py:docstring of discord.member.Member.edit:61 #: ../../../discord/member.py:docstring of discord.member.Member.request_to_speak:16 @@ -14475,7 +15123,7 @@ msgstr "このメンバーがタイムアウトされているかどうかを返 msgid "``True`` if the member is timed out. ``False`` otherwise." msgstr "メンバーがタイムアウトした場合は ``True`` 。そうでなければ、 ``False`` 。" -#: ../../api.rst:4336 +#: ../../api.rst:4570 msgid "Spotify" msgstr "Spotify" @@ -14592,7 +15240,7 @@ msgstr ":class:`datetime.timedelta`" msgid "The party ID of the listening party." msgstr "リスニングパーティーのパーティーID。" -#: ../../api.rst:4344 +#: ../../api.rst:4578 msgid "VoiceState" msgstr "VoiceState" @@ -14644,7 +15292,7 @@ msgstr "ユーザーがギルドのAFKチャンネルにいるかどうかを示 msgid "The voice channel that the user is currently connected to. ``None`` if the user is not currently in a voice channel." msgstr "ユーザーが現在接続しているボイスチャンネル。ユーザーがボイスチャンネルに接続していない場合は ``None`` 。" -#: ../../api.rst:4352 +#: ../../api.rst:4586 msgid "Emoji" msgstr "Emoji" @@ -14764,7 +15412,7 @@ msgstr "絵文字の編集中にエラーが発生した場合。" msgid "The newly updated emoji." msgstr "新しく更新された絵文字。" -#: ../../api.rst:4361 +#: ../../api.rst:4595 msgid "PartialEmoji" msgstr "PartialEmoji" @@ -14862,7 +15510,7 @@ msgstr "これがカスタム絵文字でない場合は、空の文字列が返 msgid "The PartialEmoji is not a custom emoji." msgstr "PartialEmojiがカスタム絵文字でない場合。" -#: ../../api.rst:4370 +#: ../../api.rst:4604 msgid "Role" msgstr "Role" @@ -15018,6 +15666,14 @@ msgstr "ロールをメンションすることのできる文字列を返しま msgid "Returns all the members with this role." msgstr "このロールを持つすべてのメンバーを返します。" +#: ../../../discord/role.py:docstring of discord.Role.flags:1 +msgid "Returns the role's flags." +msgstr "" + +#: ../../../discord/role.py:docstring of discord.Role.flags:5 +msgid ":class:`RoleFlags`" +msgstr "" + #: ../../../discord/role.py:docstring of discord.role.Role.edit:3 msgid "Edits the role." msgstr "ロールを編集します。" @@ -15090,7 +15746,7 @@ msgstr "ロールを削除する権限がない場合。" msgid "Deleting the role failed." msgstr "ロールの削除に失敗した場合。" -#: ../../api.rst:4378 +#: ../../api.rst:4612 msgid "RoleTags" msgstr "RoleTags" @@ -15126,7 +15782,7 @@ msgstr ":class:`bool`: ロールが購入可能かどうか。" msgid ":class:`bool`: Whether the role is a guild's linked role." msgstr ":class:`bool`: ロールがギルドの関連付けられたロールかどうか。" -#: ../../api.rst:4386 +#: ../../api.rst:4620 msgid "PartialMessageable" msgstr "PartialMessageable" @@ -15241,7 +15897,7 @@ msgstr "部分的なメッセージ。" msgid ":class:`PartialMessage`" msgstr ":class:`PartialMessage`" -#: ../../api.rst:4395 +#: ../../api.rst:4629 msgid "TextChannel" msgstr "TextChannel" @@ -15490,13 +16146,13 @@ msgid "The new ``position`` is less than 0 or greater than the number of channel msgstr "新しい ``position`` が0より小さいか、カテゴリの数より大きい場合。" #: ../../../discord/channel.py:docstring of discord.channel.TextChannel.edit:58 -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:55 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:60 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:51 msgid "You do not have permissions to edit the channel." msgstr "チャンネルを編集する権限がない場合。" #: ../../../discord/channel.py:docstring of discord.channel.TextChannel.edit:59 -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:56 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:61 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:52 msgid "Editing the channel failed." msgstr "チャンネルの編集に失敗した場合。" @@ -15874,7 +16530,7 @@ msgstr "``joined`` が ``True`` に設定され、``private`` が ``False`` に msgid ":class:`Thread` -- The archived threads." msgstr ":class:`Thread` -- アーカイブされたスレッド。" -#: ../../api.rst:4408 +#: ../../api.rst:4642 msgid "ForumChannel" msgstr "ForumChannel" @@ -15934,13 +16590,6 @@ msgstr "フォーラムに年齢制限がかかっているか。" msgid "The default auto archive duration in minutes for threads created in this forum." msgstr "このフォーラムで作成されたスレッドのデフォルトの分単位の自動アーカイブ期間。" -#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:105 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:92 -#: ../../../discord/activity.py:docstring of discord.activity.CustomActivity:33 -#: ../../../discord/channel.py:docstring of discord.channel.ForumTag:48 -msgid "Optional[:class:`PartialEmoji`]" -msgstr "Optional[:class:`PartialEmoji`]" - #: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:109 msgid "The default layout for posts in this forum channel. Defaults to :attr:`ForumLayoutType.not_set`." msgstr "このフォーラムチャネルの投稿のデフォルトの表示レイアウト。デフォルトは :attr:`ForumLayoutType.not_set` です。" @@ -15978,6 +16627,10 @@ msgstr "Optional[:class:`ForumTag`]" msgid ":class:`bool`: Checks if the forum is NSFW." msgstr ":class:`bool`: フォーラムに年齢制限があるかどうかをチェックします。" +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.is_media:1 +msgid ":class:`bool`: Checks if the channel is a media channel." +msgstr "" + #: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.edit:3 msgid "Edits the forum." msgstr "フォーラムを編集します。" @@ -16143,7 +16796,7 @@ msgstr "このフォーラムののアーカイブされたスレッドを :attr msgid "You must have :attr:`~Permissions.read_message_history` to do this." msgstr "これを行うためには、 :attr:`~Permissions.read_message_history` が必要です。" -#: ../../api.rst:4417 +#: ../../api.rst:4651 msgid "Thread" msgstr "Thread" @@ -16591,7 +17244,7 @@ msgstr "スレッドを削除する権限がない場合。" msgid "Deleting the thread failed." msgstr "スレッドの削除に失敗した場合。" -#: ../../api.rst:4430 +#: ../../api.rst:4664 msgid "ThreadMember" msgstr "ThreadMember" @@ -16631,7 +17284,7 @@ msgstr "メンバーがスレッドに参加したUTC時刻。" msgid "The thread this member belongs to." msgstr "メンバーが属するスレッド。" -#: ../../api.rst:4438 +#: ../../api.rst:4672 msgid "VoiceChannel" msgstr "VoiceChannel" @@ -16666,16 +17319,20 @@ msgstr "チャンネルの新しいユーザー人数制限。" msgid "The new region for the voice channel's voice communication. A value of ``None`` indicates automatic voice region detection." msgstr "ボイスチャンネルの新しい音声通信のためのリージョン。値が ``None`` の場合は自動で検出されます。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:54 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:53 +msgid "The new voice channel status. It can be up to 500 characters. Can be ``None`` to remove the status." +msgstr "" + +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:59 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:50 msgid "If the permission overwrite information is not in proper form." msgstr "権限の上書きの情報が適切なものでない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:58 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:63 msgid "The newly edited voice channel. If the edit was only positional then ``None`` is returned instead." msgstr "新しく編集されたボイスチャンネル。編集が位置のみだった場合は代わりに ``None`` が返されます。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:60 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:65 msgid "Optional[:class:`.VoiceChannel`]" msgstr "Optional[:class:`.VoiceChannel`]" @@ -16709,7 +17366,7 @@ msgstr "メンバーIDをキーとしボイス状態を値とするマッピン msgid "Mapping[:class:`int`, :class:`VoiceState`]" msgstr "Mapping[:class:`int`, :class:`VoiceState`]" -#: ../../api.rst:4447 +#: ../../api.rst:4681 msgid "StageChannel" msgstr "StageChannel" @@ -16771,27 +17428,31 @@ msgid "Whether to send a start notification. This sends a push notification to @ msgstr "開始通知を送信するかどうか。 ``True`` の場合、プッシュ通知を@everyoneに送信します。 デフォルトは ``False`` です。これを行うには、 :attr:`~Permissions.mention_everyone` が必要です。" #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:18 +msgid "The guild scheduled event associated with the stage instance." +msgstr "" + +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:22 msgid "The reason the stage instance was created. Shows up on the audit log." msgstr "ステージインスタンスを作成する理由。監査ログに表示されます。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:21 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:25 #: ../../../discord/stage_instance.py:docstring of discord.stage_instance.StageInstance.edit:14 msgid "If the ``privacy_level`` parameter is not the proper type." msgstr "引数 ``privacy_level`` が適切な型でない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:22 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:26 msgid "You do not have permissions to create a stage instance." msgstr "ステージインスタンスを作成する権限がない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:23 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:27 msgid "Creating a stage instance failed." msgstr "ステージインスタンスの作成に失敗した場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:25 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:29 msgid "The newly created stage instance." msgstr "新しく作成されたステージインスタンス。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:26 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:30 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.fetch_instance:11 msgid ":class:`StageInstance`" msgstr ":class:`StageInstance`" @@ -16820,7 +17481,7 @@ msgstr "新しく編集されたステージチャンネル。編集が位置の msgid "Optional[:class:`.StageChannel`]" msgstr "Optional[:class:`.StageChannel`]" -#: ../../api.rst:4457 +#: ../../api.rst:4691 msgid "StageInstance" msgstr "StageInstance" @@ -16920,7 +17581,7 @@ msgstr "ステージインスタンスを削除する権限がない場合。" msgid "Deleting the stage instance failed." msgstr "ステージインスタンスの削除に失敗した場合。" -#: ../../api.rst:4465 +#: ../../api.rst:4699 msgid "CategoryChannel" msgstr "CategoryChannel" @@ -17032,6 +17693,10 @@ msgstr "このカテゴリに属するボイスチャンネルを返します。 msgid "Returns the stage channels that are under this category." msgstr "このカテゴリに属するステージチャンネルを返します。" +#: ../../../discord/channel.py:docstring of discord.CategoryChannel.forums:1 +msgid "Returns the forum channels that are under this category." +msgstr "" + #: ../../../discord/channel.py:docstring of discord.channel.CategoryChannel.create_text_channel:3 msgid "A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category." msgstr "カテゴリ内に :class:`TextChannel` を作成するための :meth:`Guild.create_text_channel` のショートカット。" @@ -17048,7 +17713,7 @@ msgstr "カテゴリ内に :class:`StageChannel` を作成するための :meth: msgid "A shortcut method to :meth:`Guild.create_forum` to create a :class:`ForumChannel` in the category." msgstr "カテゴリ内に :class:`ForumChannel` を作成するための :meth:`Guild.create_forum` のショートカット。" -#: ../../api.rst:4474 +#: ../../api.rst:4708 msgid "DMChannel" msgstr "DMChannel" @@ -17127,7 +17792,7 @@ msgstr ":attr:`~Permissions.send_messages_in_threads`: DMにはスレッドが msgid "Thread related permissions are now set to ``False``." msgstr "スレッド関連の権限が ``False`` に設定されるようになりました。" -#: ../../api.rst:4487 +#: ../../api.rst:4721 msgid "GroupChannel" msgstr "GroupChannel" @@ -17187,7 +17852,7 @@ msgstr "あなたがグループにいる唯一の者である場合、グルー msgid "Leaving the group failed." msgstr "グループの脱退に失敗した場合。" -#: ../../api.rst:4500 +#: ../../api.rst:4734 msgid "PartialInviteGuild" msgstr "PartialInviteGuild" @@ -17254,7 +17919,7 @@ msgstr "部分的なギルドの現在の「ブースト」数。" msgid "The Discord vanity invite URL for this partial guild, if available." msgstr "存在する場合、部分的なギルドのDiscord バニティURL。" -#: ../../api.rst:4508 +#: ../../api.rst:4742 msgid "PartialInviteChannel" msgstr "PartialInviteChannel" @@ -17296,7 +17961,7 @@ msgstr "部分的なチャンネルのID。" msgid "The partial channel's type." msgstr "部分的なチャンネルの種類。" -#: ../../api.rst:4516 +#: ../../api.rst:4750 msgid "Invite" msgstr "Invite" @@ -17501,7 +18166,7 @@ msgstr "インスタント招待を取り消します。" msgid "The reason for deleting this invite. Shows up on the audit log." msgstr "招待を削除する理由。監査ログに表示されます。" -#: ../../api.rst:4524 +#: ../../api.rst:4758 msgid "Template" msgstr "Template" @@ -17604,7 +18269,7 @@ msgstr "テンプレートを削除します。" msgid "The template url." msgstr "テンプレートのURL。" -#: ../../api.rst:4532 +#: ../../api.rst:4766 msgid "WelcomeScreen" msgstr "WelcomeScreen" @@ -17676,7 +18341,7 @@ msgstr "ようこそ画面を編集するのに必要な権限がない場合。 msgid "This welcome screen does not exist." msgstr "ようこそ画面が存在しない場合。" -#: ../../api.rst:4540 +#: ../../api.rst:4774 msgid "WelcomeChannel" msgstr "WelcomeChannel" @@ -17704,7 +18369,7 @@ msgstr "チャンネルの説明の横に使用される絵文字。" msgid "Optional[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]" msgstr "Optional[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]" -#: ../../api.rst:4548 +#: ../../api.rst:4782 msgid "WidgetChannel" msgstr "WidgetChannel" @@ -17720,7 +18385,7 @@ msgstr "チャンネルのID。" msgid "The channel's position" msgstr "チャンネルの位置。" -#: ../../api.rst:4556 +#: ../../api.rst:4790 msgid "WidgetMember" msgstr "WidgetMember" @@ -17804,7 +18469,7 @@ msgstr "Optional[:class:`WidgetChannel`]" msgid "Returns the member's display name." msgstr "メンバーの表示名を返します。" -#: ../../api.rst:4565 +#: ../../api.rst:4799 msgid "Widget" msgstr "Widget" @@ -17876,7 +18541,7 @@ msgstr "招待にカウント情報を含めるかどうか。これにより :a msgid "The invite from the widget's invite URL, if available." msgstr "利用可能な場合は、ウィジェットの招待URLからの招待。" -#: ../../api.rst:4573 +#: ../../api.rst:4807 msgid "StickerPack" msgstr "StickerPack" @@ -17936,7 +18601,7 @@ msgstr "Optional[:class:`StandardSticker`]" msgid "The banner asset of the sticker pack." msgstr "スタンプパックのバナーアセット。" -#: ../../api.rst:4581 +#: ../../api.rst:4815 msgid "StickerItem" msgstr "StickerItem" @@ -17990,7 +18655,7 @@ msgstr "スタンプアイテムの完全なスタンプデータを取得する msgid "Union[:class:`StandardSticker`, :class:`GuildSticker`]" msgstr "Union[:class:`StandardSticker`, :class:`GuildSticker`]" -#: ../../api.rst:4589 +#: ../../api.rst:4823 msgid "Sticker" msgstr "Sticker" @@ -18031,7 +18696,7 @@ msgstr "スタンプパックのID。" msgid "Returns the sticker's creation time in UTC." msgstr "スタンプの作成された時間をUTCで返します。" -#: ../../api.rst:4597 +#: ../../api.rst:4831 msgid "StandardSticker" msgstr "StandardSticker" @@ -18067,7 +18732,7 @@ msgstr "取得したスタンプパック。" msgid ":class:`StickerPack`" msgstr ":class:`StickerPack`" -#: ../../api.rst:4605 +#: ../../api.rst:4839 msgid "GuildSticker" msgstr "GuildSticker" @@ -18123,7 +18788,7 @@ msgstr "スタンプの編集中にエラーが発生した場合。" msgid "The newly modified sticker." msgstr "新しく変更されたスタンプ。" -#: ../../api.rst:4613 +#: ../../api.rst:4847 msgid "ShardInfo" msgstr "ShardInfo" @@ -18167,7 +18832,127 @@ msgstr "シャードを接続します。もしすでに接続されている場 msgid "Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard." msgstr "このシャードのHEARTBEATとHEARTBEAT_ACK間の待ち時間を秒単位で測定します。" -#: ../../api.rst:4621 +#: ../../api.rst:4855 +msgid "SKU" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:1 +msgid "Represents a premium offering as a stock-keeping unit (SKU)." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:7 +msgid "The SKU's ID." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:13 +msgid "The type of the SKU." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:15 +msgid ":class:`SKUType`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:19 +msgid "The ID of the application that the SKU belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:25 +msgid "The consumer-facing name of the premium offering." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:31 +msgid "A system-generated URL slug based on the SKU name." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.flags:1 +msgid "Returns the flags of the SKU." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.flags:3 +msgid ":class:`SKUFlags`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.created_at:1 +msgid "Returns the sku's creation time in UTC." +msgstr "" + +#: ../../api.rst:4863 +msgid "Entitlement" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:1 +msgid "Represents an entitlement from user or guild which has been granted access to a premium offering." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:7 +msgid "The entitlement's ID." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:13 +msgid "The ID of the SKU that the entitlement belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:19 +msgid "The ID of the application that the entitlement belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:25 +msgid "The ID of the user that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:31 +msgid "The type of the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:33 +msgid ":class:`EntitlementType`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:37 +msgid "Whether the entitlement has been deleted." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:43 +msgid "A UTC start date which the entitlement is valid. Not present when using test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:49 +msgid "A UTC date which entitlement is no longer valid. Not present when using test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:55 +msgid "The ID of the guild that is granted access to the entitlement" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.user:1 +msgid "The user that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.guild:1 +msgid "The guild that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.created_at:1 +msgid "Returns the entitlement's creation time in UTC." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.is_expired:1 +msgid ":class:`bool`: Returns ``True`` if the entitlement is expired. Will be always False for test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:3 +msgid "Deletes the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:6 +msgid "The entitlement could not be found." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:7 +msgid "Deleting the entitlement failed." +msgstr "" + +#: ../../api.rst:4871 msgid "RawMessageDeleteEvent" msgstr "RawMessageDeleteEvent" @@ -18192,7 +18977,7 @@ msgstr "削除されたメッセージ ID。" msgid "The cached message, if found in the internal message cache." msgstr "内部のメッセージキャッシュに見つかった場合、そのキャッシュされたメッセージ。" -#: ../../api.rst:4629 +#: ../../api.rst:4879 msgid "RawBulkMessageDeleteEvent" msgstr "RawBulkMessageDeleteEvent" @@ -18220,7 +19005,7 @@ msgstr "内部のメッセージキャッシュに見つかった場合、その msgid "List[:class:`Message`]" msgstr "List[:class:`Message`]" -#: ../../api.rst:4637 +#: ../../api.rst:4887 msgid "RawMessageUpdateEvent" msgstr "RawMessageUpdateEvent" @@ -18247,8 +19032,8 @@ msgstr ":ddocs:`ゲートウェイ ` によって #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawMessageUpdateEvent:29 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawThreadUpdateEvent:33 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawThreadMembersUpdate:27 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:57 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:69 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:65 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:77 msgid ":class:`dict`" msgstr ":class:`dict`" @@ -18256,7 +19041,7 @@ msgstr ":class:`dict`" msgid "The cached message, if found in the internal message cache. Represents the message before it is modified by the data in :attr:`RawMessageUpdateEvent.data`." msgstr "内部メッセージキャッシュで見つかった場合、そのキャッシュされたメッセージ。 :attr:`RawMessageUpdateEvent.data` のデータによって変更される前のメッセージを表します。" -#: ../../api.rst:4645 +#: ../../api.rst:4895 msgid "RawReactionActionEvent" msgstr "RawReactionActionEvent" @@ -18289,10 +19074,30 @@ msgid "The member who added the reaction. Only available if ``event_type`` is `` msgstr "リアクションを追加したメンバー。 ``event_type`` が ``REACTION_ADD`` でリアクションがギルド内にある場合にのみ利用できます。" #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:44 +msgid "The author ID of the message being reacted to. Only available if ``event_type`` is ``REACTION_ADD``." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:52 msgid "The event type that triggered this action. Can be ``REACTION_ADD`` for reaction addition or ``REACTION_REMOVE`` for reaction removal." msgstr "このアクションの原因であるイベントタイプ。リアクションの追加は ``REACTION_ADD`` 、リアクションの除去は ``REACTION_REMOVE`` です。" -#: ../../api.rst:4653 +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:62 +msgid "Whether the reaction was a burst reaction, also known as a \"super reaction\"." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:70 +msgid "A list of colours used for burst reaction animation. Only available if ``burst`` is ``True`` and if ``event_type`` is ``REACTION_ADD``." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:75 +msgid "List[:class:`Colour`]" +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.RawReactionActionEvent.burst_colors:1 +msgid "An alias of :attr:`burst_colours`." +msgstr "" + +#: ../../api.rst:4903 msgid "RawReactionClearEvent" msgstr "RawReactionClearEvent" @@ -18315,7 +19120,7 @@ msgstr "リアクションの一括除去が行われたチャンネルのID。" msgid "The guild ID where the reactions got cleared." msgstr "リアクションの一括除去が行われたギルドのID。" -#: ../../api.rst:4661 +#: ../../api.rst:4911 msgid "RawReactionClearEmojiEvent" msgstr "RawReactionClearEmojiEvent" @@ -18327,7 +19132,7 @@ msgstr ":func:`on_raw_reaction_clear_emoji` イベントのペイロードを表 msgid "The custom or unicode emoji being removed." msgstr "除去されたカスタムまたはユニコード絵文字。" -#: ../../api.rst:4669 +#: ../../api.rst:4919 msgid "RawIntegrationDeleteEvent" msgstr "RawIntegrationDeleteEvent" @@ -18347,7 +19152,7 @@ msgstr "削除された連携サービスのボットやOAuth2 アプリケー msgid "The guild ID where the integration got deleted." msgstr "連携サービスが削除されたギルドのID。" -#: ../../api.rst:4677 +#: ../../api.rst:4927 msgid "RawThreadUpdateEvent" msgstr "RawThreadUpdateEvent" @@ -18392,7 +19197,7 @@ msgstr "スレッドが内部キャッシュで見つかった場合、そのス msgid "Optional[:class:`discord.Thread`]" msgstr "Optional[:class:`discord.Thread`]" -#: ../../api.rst:4685 +#: ../../api.rst:4935 msgid "RawThreadMembersUpdate" msgstr "RawThreadMembersUpdate" @@ -18408,7 +19213,7 @@ msgstr "スレッドのおおよそのメンバー数。この値は50の上限 msgid "The raw data given by the :ddocs:`gateway `." msgstr ":ddocs:`ゲートウェイ ` によって与えられた生のデータ。" -#: ../../api.rst:4693 +#: ../../api.rst:4943 msgid "RawThreadDeleteEvent" msgstr "RawThreadDeleteEvent" @@ -18432,7 +19237,7 @@ msgstr "スレッドが削除されたギルドのID。" msgid "The ID of the channel the thread belonged to." msgstr "スレッドが属したチャンネルの ID。" -#: ../../api.rst:4701 +#: ../../api.rst:4951 msgid "RawTypingEvent" msgstr "RawTypingEvent" @@ -18460,7 +19265,7 @@ msgstr "Optional[Union[:class:`discord.User`, :class:`discord.Member`]]" msgid "The ID of the guild the user started typing in, if applicable." msgstr "該当する場合、ユーザーが入力し始めたギルドのID。" -#: ../../api.rst:4709 +#: ../../api.rst:4959 msgid "RawMemberRemoveEvent" msgstr "RawMemberRemoveEvent" @@ -18480,7 +19285,7 @@ msgstr "Union[:class:`discord.User`, :class:`discord.Member`]" msgid "The ID of the guild the user left." msgstr "ユーザーが脱退したギルドのID。" -#: ../../api.rst:4717 +#: ../../api.rst:4967 msgid "RawAppCommandPermissionsUpdateEvent" msgstr "RawAppCommandPermissionsUpdateEvent" @@ -18504,7 +19309,7 @@ msgstr "権限が更新されたギルド。" msgid "List of new permissions for the app command." msgstr "アプリケーションコマンドの新しい権限のリスト。" -#: ../../api.rst:4725 +#: ../../api.rst:4975 msgid "PartialWebhookGuild" msgstr "PartialWebhookGuild" @@ -18517,7 +19322,7 @@ msgstr "Webhook用の部分的なギルドを表します。" msgid "These are typically given for channel follower webhooks." msgstr "これは通常、チャンネルをフォローするWebhookから与えられます。" -#: ../../api.rst:4733 +#: ../../api.rst:4983 msgid "PartialWebhookChannel" msgstr "PartialWebhookChannel" @@ -18525,23 +19330,23 @@ msgstr "PartialWebhookChannel" msgid "Represents a partial channel for webhooks." msgstr "Webhook用の部分的なチャンネルを表します。" -#: ../../api.rst:4743 +#: ../../api.rst:4993 msgid "Data Classes" msgstr "データクラス" -#: ../../api.rst:4745 +#: ../../api.rst:4995 msgid "Some classes are just there to be data containers, this lists them." msgstr "一部のクラスはデータコンテナとして用いられます。ここではそのクラスを一覧表にしています。" -#: ../../api.rst:4747 +#: ../../api.rst:4997 msgid "Unlike :ref:`models ` you are allowed to create most of these yourself, even if they can also be used to hold attributes." msgstr ":ref:`models ` とは異なり、属性を持つものであっても、自分で作成することが許されています。" -#: ../../api.rst:4753 +#: ../../api.rst:5003 msgid "The only exception to this rule is :class:`Object`, which is made with dynamic attributes in mind." msgstr "このルールの唯一の例外は :class:`Object` で、動的な属性を念頭に置いて作成されます。" -#: ../../api.rst:4758 +#: ../../api.rst:5008 msgid "Object" msgstr "Object" @@ -18589,7 +19394,7 @@ msgstr "Type[:class:`abc.Snowflake`]" msgid "Returns the snowflake's creation time in UTC." msgstr "スノーフレークの作成時刻をUTCで返します。" -#: ../../api.rst:4766 +#: ../../api.rst:5016 msgid "Embed" msgstr "Embed" @@ -18902,7 +19707,7 @@ msgstr "無効なインデックスが指定された場合。" msgid "Converts this embed object into a dict." msgstr "埋め込みオブジェクトを辞書型に変換します。" -#: ../../api.rst:4774 +#: ../../api.rst:5024 msgid "AllowedMentions" msgstr "AllowedMentions" @@ -18943,7 +19748,7 @@ msgstr "すべてのフィールドが ``True`` に明示的に設定された : msgid "A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``" msgstr "すべてのフィールドが ``False`` に設定された :class:`AllowedMentions` を返すファクトリメソッド。" -#: ../../api.rst:4782 +#: ../../api.rst:5032 msgid "MessageReference" msgstr "MessageReference" @@ -19008,7 +19813,7 @@ msgstr "Optional[:class:`~discord.Message`]" msgid "Returns a URL that allows the client to jump to the referenced message." msgstr "クライアントが参照されたメッセージにジャンプすることのできるURLを返します。" -#: ../../api.rst:4790 +#: ../../api.rst:5040 msgid "PartialMessage" msgstr "PartialMessage" @@ -19068,7 +19873,11 @@ msgstr "該当する場合、この部分的なメッセージが属するギル msgid "The partial message's creation time in UTC." msgstr "UTCの、部分的なメッセージが作成された時刻。" -#: ../../api.rst:4798 +#: ../../../discord/message.py:docstring of discord.PartialMessage.thread:5 +msgid "This does not retrieve archived threads, as they are not retained in the internal cache. Use :meth:`fetch_thread` instead." +msgstr "" + +#: ../../api.rst:5048 msgid "MessageApplication" msgstr "MessageApplication" @@ -19084,7 +19893,7 @@ msgstr "存在する場合、アプリケーションのアイコン。" msgid "The application's cover image, if any." msgstr "存在する場合、アプリケーションのカバー画像。" -#: ../../api.rst:4806 +#: ../../api.rst:5056 msgid "RoleSubscriptionInfo" msgstr "RoleSubscriptionInfo" @@ -19112,7 +19921,7 @@ msgstr "ユーザーが購読している月数の合計。" msgid "Whether this notification is for a renewal rather than a new purchase." msgstr "この通知が新しい購入ではなく、更新のためであるかどうか。" -#: ../../api.rst:4814 +#: ../../api.rst:5064 msgid "Intents" msgstr "Intents" @@ -19894,7 +20703,7 @@ msgstr "自動管理ルール対応関係のイベントが有効になってい msgid "This corresponds to the following events: - :func:`on_automod_action`" msgstr "これは以下のイベントに対応します: - :func:`on_automod_action`" -#: ../../api.rst:4822 +#: ../../api.rst:5072 msgid "MemberCacheFlags" msgstr "MemberCacheFlags" @@ -19986,7 +20795,7 @@ msgstr "結果として生成されるメンバーキャッシュフラグ。" msgid ":class:`MemberCacheFlags`" msgstr ":class:`MemberCacheFlags`" -#: ../../api.rst:4830 +#: ../../api.rst:5080 msgid "ApplicationFlags" msgstr "ApplicationFlags" @@ -20070,7 +20879,7 @@ msgstr "アプリケーションがグローバルアプリケーションコマ msgid "Returns ``True`` if the application has had at least one global application command used in the last 30 days." msgstr "過去30日間で少なくとも1つのグローバルアプリケーションコマンドが使用されている場合に ``True`` を返します。" -#: ../../api.rst:4838 +#: ../../api.rst:5088 msgid "ChannelFlags" msgstr "ChannelFlags" @@ -20110,7 +20919,11 @@ msgstr "スレッドがフォーラムチャンネルにピン留めされてい msgid "Returns ``True`` if a tag is required to be specified when creating a thread in a :class:`ForumChannel`." msgstr ":class:`ForumChannel` でスレッドを作成する際にタグを指定する必要がある場合に ``True`` を返します。" -#: ../../api.rst:4846 +#: ../../docstring of discord.ChannelFlags.hide_media_download_options:1 +msgid "Returns ``True`` if the client hides embedded media download options in a :class:`ForumChannel`. Only available in media channels." +msgstr "" + +#: ../../api.rst:5096 msgid "AutoModPresets" msgstr "AutoModPresets" @@ -20162,7 +20975,7 @@ msgstr "すべて有効化された :class:`AutoModPresets` を作成するフ msgid "A factory method that creates a :class:`AutoModPresets` with everything disabled." msgstr "すべて無効化された :class:`AutoModPresets` を作成するファクトリメソッド。" -#: ../../api.rst:4854 +#: ../../api.rst:5104 msgid "AutoModRuleAction" msgstr "AutoModRuleAction" @@ -20198,7 +21011,7 @@ msgstr "Optional[:class:`datetime.timedelta`]" msgid "A custom message which will be shown to a user when their message is blocked. Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.block_message`." msgstr "メッセージがブロックされたときに送信者に表示されるカスタムメッセージ。 :attr:`type` を :attr:`~AutoModRuleActionType.block_message` に設定します。" -#: ../../api.rst:4862 +#: ../../api.rst:5112 msgid "AutoModTrigger" msgstr "AutoModTrigger" @@ -20223,6 +21036,7 @@ msgid ":attr:`AutoModRuleTriggerType.keyword`" msgstr ":attr:`AutoModRuleTriggerType.keyword`" #: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:8 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:18 msgid ":attr:`keyword_filter`, :attr:`regex_patterns`, :attr:`allow_list`" msgstr ":attr:`keyword_filter`, :attr:`regex_patterns`, :attr:`allow_list`" @@ -20243,46 +21057,54 @@ msgid ":attr:`AutoModRuleTriggerType.mention_spam`" msgstr ":attr:`AutoModRuleTriggerType.mention_spam`" #: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:15 -msgid ":attr:`mention_limit`" -msgstr ":attr:`mention_limit`" +msgid ":attr:`mention_limit`, :attr:`mention_raid_protection`" +msgstr "" + +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:18 +msgid ":attr:`AutoModRuleTriggerType.member_profile`" +msgstr "" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:22 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:26 msgid "The type of trigger." msgstr "発動条件の種類。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:28 -msgid "The list of strings that will trigger the keyword filter. Maximum of 1000. Keywords can only be up to 60 characters in length." -msgstr "キーワードフィルタを発動させる文字列の一覧。最大1000個まで。キーワードは各60文字以内です。" +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:32 +msgid "The list of strings that will trigger the filter. Maximum of 1000. Keywords can only be up to 60 characters in length." +msgstr "" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:31 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:35 msgid "This could be combined with :attr:`regex_patterns`." msgstr ":attr:`regex_patterns` と組み合わせることができます。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:37 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:41 msgid "The regex pattern that will trigger the filter. The syntax is based off of `Rust's regex syntax `_. Maximum of 10. Regex strings can only be up to 260 characters in length." msgstr "フィルタを発動させる正規表現パターン。構文は `Rust の正規表現構文 `_ に基づいています。 最大 10 個まで。正規表現文字列は 260 文字までしか使用できません。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:41 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:45 msgid "This could be combined with :attr:`keyword_filter` and/or :attr:`allow_list`" msgstr ":attr:`keyword_filter` や :attr:`allow_list` と組み合わせることができます。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:49 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:53 msgid "The presets used with the preset keyword filter." msgstr "プリセットキーワードフィルタで使用されるプリセット。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:51 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:55 msgid ":class:`AutoModPresets`" msgstr ":class:`AutoModPresets`" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:55 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:59 msgid "The list of words that are exempt from the commonly flagged words. Maximum of 100. Keywords can only be up to 60 characters in length." msgstr "共通のキーワードフィルタの単語から除外される単語の一覧。最大100個まで。キーワードは各60文字以内です。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:62 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:66 msgid "The total number of user and role mentions a message can contain. Has a maximum of 50." msgstr "メッセージに含めることのできるユーザーとロールのメンションの合計数。最大で50個です。" -#: ../../api.rst:4870 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:73 +msgid "Whether mention raid protection is enabled or not." +msgstr "" + +#: ../../api.rst:5120 msgid "File" msgstr "File" @@ -20322,7 +21144,7 @@ msgstr "表示するファイルの説明。現在画像でのみサポートさ msgid "The filename to display when uploading to Discord. If this is not given then it defaults to ``fp.name`` or if ``fp`` is a string then the ``filename`` will default to the string given." msgstr "Discordにアップロードするときに表示されるファイル名。指定されていない場合はデフォルトでは ``fp.name`` 、または ``fp`` が文字列の場合、 ``filename`` は与えられた文字列をデフォルトにします。" -#: ../../api.rst:4878 +#: ../../api.rst:5128 msgid "Colour" msgstr "Colour" @@ -20570,7 +21392,7 @@ msgstr "``0xEEEFF1`` の値を持つ :class:`Colour` を返すクラスメソッ msgid "A factory method that returns a :class:`Colour` with a value of ``0xEB459F``." msgstr "``0xEB459F`` の値を持つ :class:`Colour` を返すクラスメソッドです。" -#: ../../api.rst:4886 +#: ../../api.rst:5136 msgid "BaseActivity" msgstr "BaseActivity" @@ -20608,7 +21430,7 @@ msgstr "なお、ライブラリはこれらをユーザー設定可能としま msgid "When the user started doing this activity in UTC." msgstr "ユーザーがアクティビティを開始したときのUTC時刻。" -#: ../../api.rst:4894 +#: ../../api.rst:5144 msgid "Activity" msgstr "Activity" @@ -20649,54 +21471,62 @@ msgid "The detail of the user's current activity." msgstr "ユーザーの現在のアクティビティの詳細。" #: ../../../discord/activity.py:docstring of discord.activity.Activity:50 +msgid "The user's current platform." +msgstr "" + +#: ../../../discord/activity.py:docstring of discord.activity.Activity:58 msgid "A dictionary of timestamps. It contains the following optional keys:" msgstr "タイムスタンプの辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:52 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:60 msgid "``start``: Corresponds to when the user started doing the activity in milliseconds since Unix epoch." msgstr "``start``: ユーザーがアクティビティを開始したときのUnixエポック起算ミリ秒数に対応します。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:54 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:62 msgid "``end``: Corresponds to when the user will finish doing the activity in milliseconds since Unix epoch." msgstr "``end``: ユーザーがアクティビティを終了する予定時刻のUnixエポック起算ミリ秒数に対応します。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:61 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:69 msgid "A dictionary representing the images and their hover text of an activity. It contains the following optional keys:" msgstr "アクティビティの画像とそれらのホバーテキストを表す辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:64 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:72 +#: ../../../discord/activity.py:docstring of discord.activity.Game:45 msgid "``large_image``: A string representing the ID for the large image asset." msgstr "``large_image``: 大きな画像アセットのIDを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:65 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:73 +#: ../../../discord/activity.py:docstring of discord.activity.Game:46 msgid "``large_text``: A string representing the text when hovering over the large image asset." msgstr "``large_text``: 大きな画像アセットをホバーしたときに表示するテキストを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:66 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:74 +#: ../../../discord/activity.py:docstring of discord.activity.Game:47 msgid "``small_image``: A string representing the ID for the small image asset." msgstr "``small_image``: 小さな画像アセットのIDを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:67 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:75 +#: ../../../discord/activity.py:docstring of discord.activity.Game:48 msgid "``small_text``: A string representing the text when hovering over the small image asset." msgstr "``small_text``: 小さな画像アセットをホバーしたときに表示するテキストを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:73 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:81 msgid "A dictionary representing the activity party. It contains the following optional keys:" msgstr "アクティビティのパーティーを表す辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:75 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:83 msgid "``id``: A string representing the party ID." msgstr "``id``: パーティー ID を表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:76 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:84 msgid "``size``: A list of up to two integer elements denoting (current_size, maximum_size)." msgstr "``size``: 現在の大きさと最大の大きさをである二個以内の整数のリスト。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:82 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:90 msgid "A list of strings representing the labels of custom buttons shown in a rich presence." msgstr "リッチプレゼンスに表示されるカスタムボタンのラベルを表す文字列のリスト。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:90 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:98 msgid "The emoji that belongs to this activity." msgstr "このアクティビティに属する絵文字。" @@ -20724,7 +21554,7 @@ msgstr "該当する場合、このアクティビティの大きな画像アセ msgid "Returns the small image asset hover text of this activity, if applicable." msgstr "該当する場合、このアクティビティの小さな画像アセットのホバーテキストを返します。" -#: ../../api.rst:4902 +#: ../../api.rst:5152 msgid "Game" msgstr "Game" @@ -20757,6 +21587,14 @@ msgstr "ゲームの名前を返します。" msgid "The game's name." msgstr "ゲームの名前。" +#: ../../../discord/activity.py:docstring of discord.activity.Game:34 +msgid "Where the user is playing from (ie. PS5, Xbox)." +msgstr "" + +#: ../../../discord/activity.py:docstring of discord.activity.Game:42 +msgid "A dictionary representing the images and their hover text of a game. It contains the following optional keys:" +msgstr "" + #: ../../../discord/activity.py:docstring of discord.Game.type:1 #: ../../../discord/activity.py:docstring of discord.Streaming.type:1 msgid "Returns the game's type. This is for compatibility with :class:`Activity`." @@ -20774,7 +21612,7 @@ msgstr "該当する場合、ユーザーがゲームを開始したときのUTC msgid "When the user will stop playing this game in UTC, if applicable." msgstr "該当する場合、ユーザーがゲームを終了する予定のUTC時刻。" -#: ../../api.rst:4910 +#: ../../api.rst:5160 msgid "Streaming" msgstr "Streaming" @@ -20838,7 +21676,7 @@ msgstr "提供された場合、ストリーム中のユーザーのTwitchの名 msgid "This corresponds to the ``large_image`` key of the :attr:`Streaming.assets` dictionary if it starts with ``twitch:``. Typically set by the Discord client." msgstr "これが ``twitch:`` で始まる場合、 :attr:`Streaming.assets` 辞書の ``large_image`` キーに対応します。典型的にはDiscordクライアントによって設定されます。" -#: ../../api.rst:4918 +#: ../../api.rst:5168 msgid "CustomActivity" msgstr "CustomActivity" @@ -20862,7 +21700,7 @@ msgstr "存在する場合、アクティビティに渡す絵文字。" msgid "It always returns :attr:`ActivityType.custom`." msgstr "これは常に :attr:`ActivityType.custom` を返します。" -#: ../../api.rst:4926 +#: ../../api.rst:5176 msgid "Permissions" msgstr "Permissions" @@ -21105,6 +21943,10 @@ msgstr ":attr:`manage_threads`" msgid ":attr:`moderate_members`" msgstr ":attr:`moderate_members`" +#: ../../../discord/permissions.py:docstring of discord.permissions.Permissions.events:1 +msgid "A factory method that creates a :class:`Permissions` with all \"Events\" permissions from the official Discord UI set to ``True``." +msgstr "" + #: ../../../discord/permissions.py:docstring of discord.permissions.Permissions.advanced:1 msgid "A factory method that creates a :class:`Permissions` with all \"Advanced\" permissions from the official Discord UI set to ``True``." msgstr "Discord公式UIの「高度な権限」をすべて ``True`` に設定した :class:`Permissions` を作成するファクトリメソッド。" @@ -21323,6 +22165,10 @@ msgstr "ユーザーがボイスチャンネルにて埋め込みアプリケー msgid "Returns ``True`` if a user can time out other members." msgstr "ユーザーが他のユーザーをタイムアウトできる場合は ``True`` を返します。" +#: ../../docstring of discord.Permissions.view_creator_monetization_analytics:1 +msgid "Returns ``True`` if a user can view role subscription insights." +msgstr "" + #: ../../docstring of discord.Permissions.use_soundboard:1 msgid "Returns ``True`` if a user can use the soundboard." msgstr "ユーザーがサウンドボードを使用できる場合は ``True`` を返します。" @@ -21331,6 +22177,10 @@ msgstr "ユーザーがサウンドボードを使用できる場合は ``True`` msgid "Returns ``True`` if a user can create emojis, stickers, and soundboard sounds." msgstr "絵文字、スタンプ、サウンドボードのサウンドを作成できる場合は ``True`` を返します。" +#: ../../docstring of discord.Permissions.create_events:1 +msgid "Returns ``True`` if a user can create guild events." +msgstr "" + #: ../../docstring of discord.Permissions.use_external_sounds:1 msgid "Returns ``True`` if a user can use sounds from other guilds." msgstr "ユーザーが他のギルドのサウンドを使用できる場合は ``True`` を返します。" @@ -21339,7 +22189,7 @@ msgstr "ユーザーが他のギルドのサウンドを使用できる場合は msgid "Returns ``True`` if a user can send voice messages." msgstr "ユーザーがボイスメッセージを送信できる場合は ``True`` を返します。" -#: ../../api.rst:4934 +#: ../../api.rst:5184 msgid "PermissionOverwrite" msgstr "PermissionOverwrite" @@ -21395,7 +22245,7 @@ msgstr "権限上書きオブジェクトを一括更新します。" msgid "A list of key/value pairs to bulk update with." msgstr "一括更新するためのキーと値のペアのリスト。" -#: ../../api.rst:4942 +#: ../../api.rst:5192 msgid "SystemChannelFlags" msgstr "SystemChannelFlags" @@ -21453,7 +22303,7 @@ msgstr "ロールサブスクリプションの購入と更新通知が有効に msgid "Returns ``True`` if the role subscription notifications have a sticker reply button." msgstr "ロールサブスクリプション通知にスタンプの返信ボタンがある場合に ``True`` を返します。" -#: ../../api.rst:4950 +#: ../../api.rst:5200 msgid "MessageFlags" msgstr "MessageFlags" @@ -21533,7 +22383,7 @@ msgstr ":attr:`suppress_notifications` のエイリアス。" msgid "Returns ``True`` if the message is a voice message." msgstr "メッセージがボイスメッセージの場合に ``True`` を返します。" -#: ../../api.rst:4958 +#: ../../api.rst:5208 msgid "PublicUserFlags" msgstr "PublicUserFlags" @@ -21641,7 +22491,7 @@ msgstr "ユーザーがアクティブな開発者の場合に ``True`` を返 msgid "List[:class:`UserFlags`]: Returns all public flags the user has." msgstr "List[:class:`UserFlags`]: ユーザーが持つすべての公開フラグを返します。" -#: ../../api.rst:4966 +#: ../../api.rst:5216 msgid "MemberFlags" msgstr "MemberFlags" @@ -21689,7 +22539,131 @@ msgstr "メンバーがギルドの認証要件をバイパスできる場合に msgid "Returns ``True`` if the member has started onboarding." msgstr "メンバーがオンボーディングを開始した場合に ``True`` を返します。" -#: ../../api.rst:4974 +#: ../../api.rst:5224 +msgid "AttachmentFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:1 +msgid "Wraps up the Discord Attachment flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:9 +msgid "Checks if two AttachmentFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:13 +msgid "Checks if two AttachmentFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:17 +msgid "Returns a AttachmentFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:22 +msgid "Returns a AttachmentFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:27 +msgid "Returns a AttachmentFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:32 +msgid "Returns a AttachmentFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.clip:1 +msgid "Returns ``True`` if the attachment is a clip." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.thumbnail:1 +msgid "Returns ``True`` if the attachment is a thumbnail." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.remix:1 +msgid "Returns ``True`` if the attachment has been edited using the remix feature." +msgstr "" + +#: ../../api.rst:5232 +msgid "RoleFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:1 +msgid "Wraps up the Discord Role flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:9 +msgid "Checks if two RoleFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:13 +msgid "Checks if two RoleFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:17 +msgid "Returns a RoleFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:22 +msgid "Returns a RoleFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:27 +msgid "Returns a RoleFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:32 +msgid "Returns a RoleFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.RoleFlags.in_prompt:1 +msgid "Returns ``True`` if the role can be selected by members in an onboarding prompt." +msgstr "" + +#: ../../api.rst:5240 +msgid "SKUFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:1 +msgid "Wraps up the Discord SKU flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:9 +msgid "Checks if two SKUFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:13 +msgid "Checks if two SKUFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:17 +msgid "Returns a SKUFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:22 +msgid "Returns a SKUFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:27 +msgid "Returns a SKUFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:32 +msgid "Returns a SKUFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.SKUFlags.available:1 +msgid "Returns ``True`` if the SKU is available for purchase." +msgstr "" + +#: ../../docstring of discord.SKUFlags.guild_subscription:1 +msgid "Returns ``True`` if the SKU is a guild subscription." +msgstr "" + +#: ../../docstring of discord.SKUFlags.user_subscription:1 +msgid "Returns ``True`` if the SKU is a user subscription." +msgstr "" + +#: ../../api.rst:5248 msgid "ForumTag" msgstr "ForumTag" @@ -21725,11 +22699,11 @@ msgstr ":attr:`~Permissions.manage_threads` 権限を有するモデレータの msgid "The emoji that is used to represent this tag. Note that if the emoji is a custom emoji, it will *not* have name information." msgstr "このタグを表すために使用される絵文字。絵文字がカスタム絵文字の場合、名前情報は *提供されません* 。" -#: ../../api.rst:4983 +#: ../../api.rst:5257 msgid "Exceptions" msgstr "例外" -#: ../../api.rst:4985 +#: ../../api.rst:5259 msgid "The following exceptions are thrown by the library." msgstr "以下の例外がライブラリにより送出されます。" @@ -21887,67 +22861,67 @@ msgstr "返されたエラーコード。" msgid "An exception that is thrown for when libopus is not loaded." msgstr "libopus がロードされていないときに送出される例外。" -#: ../../api.rst:5020 +#: ../../api.rst:5294 msgid "Exception Hierarchy" msgstr "例外の階層構造" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`Exception`" msgstr ":exc:`Exception`" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`DiscordException`" msgstr ":exc:`DiscordException`" -#: ../../api.rst:5030 +#: ../../api.rst:5304 msgid ":exc:`ClientException`" msgstr ":exc:`ClientException`" -#: ../../api.rst:5027 +#: ../../api.rst:5301 msgid ":exc:`InvalidData`" msgstr ":exc:`InvalidData`" -#: ../../api.rst:5028 +#: ../../api.rst:5302 msgid ":exc:`LoginFailure`" msgstr ":exc:`LoginFailure`" -#: ../../api.rst:5029 +#: ../../api.rst:5303 msgid ":exc:`ConnectionClosed`" msgstr ":exc:`ConnectionClosed`" -#: ../../api.rst:5030 +#: ../../api.rst:5304 msgid ":exc:`PrivilegedIntentsRequired`" msgstr ":exc:`PrivilegedIntentsRequired`" -#: ../../api.rst:5031 +#: ../../api.rst:5305 msgid ":exc:`InteractionResponded`" msgstr ":exc:`InteractionResponded`" -#: ../../api.rst:5032 +#: ../../api.rst:5306 msgid ":exc:`GatewayNotFound`" msgstr ":exc:`GatewayNotFound`" -#: ../../api.rst:5036 +#: ../../api.rst:5310 msgid ":exc:`HTTPException`" msgstr ":exc:`HTTPException`" -#: ../../api.rst:5034 +#: ../../api.rst:5308 msgid ":exc:`Forbidden`" msgstr ":exc:`Forbidden`" -#: ../../api.rst:5035 +#: ../../api.rst:5309 msgid ":exc:`NotFound`" msgstr ":exc:`NotFound`" -#: ../../api.rst:5036 +#: ../../api.rst:5310 msgid ":exc:`DiscordServerError`" msgstr ":exc:`DiscordServerError`" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`app_commands.CommandSyncFailure`" msgstr ":exc:`app_commands.CommandSyncFailure`" -#: ../../api.rst:5038 +#: ../../api.rst:5312 msgid ":exc:`RateLimited`" msgstr ":exc:`RateLimited`" diff --git a/docs/locale/ja/LC_MESSAGES/discord.po b/docs/locale/ja/LC_MESSAGES/discord.po index 55d1bdc6bbd9..66070b699c7a 100644 --- a/docs/locale/ja/LC_MESSAGES/discord.po +++ b/docs/locale/ja/LC_MESSAGES/discord.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po b/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po index 4099131ca160..07125ecc4a7d 100644 --- a/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po +++ b/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/faq.po b/docs/locale/ja/LC_MESSAGES/faq.po index c92e82a4a42d..4fb1dddff903 100644 --- a/docs/locale/ja/LC_MESSAGES/faq.po +++ b/docs/locale/ja/LC_MESSAGES/faq.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/intents.po b/docs/locale/ja/LC_MESSAGES/intents.po index ab439dba0661..907f1e79e72f 100644 --- a/docs/locale/ja/LC_MESSAGES/intents.po +++ b/docs/locale/ja/LC_MESSAGES/intents.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/intro.po b/docs/locale/ja/LC_MESSAGES/intro.po index ca6937854784..93acdbe80f85 100644 --- a/docs/locale/ja/LC_MESSAGES/intro.po +++ b/docs/locale/ja/LC_MESSAGES/intro.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/logging.po b/docs/locale/ja/LC_MESSAGES/logging.po index 05ef05165c20..d5a6b114dcb6 100644 --- a/docs/locale/ja/LC_MESSAGES/logging.po +++ b/docs/locale/ja/LC_MESSAGES/logging.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -46,18 +46,22 @@ msgid "This is recommended, especially at verbose levels such as ``DEBUG``, as t msgstr "特に、 ``DEBUG`` といった冗長なイベントレベルを設定している場合、プログラムの標準エラー出力をつまらせてしまう原因になるため、ファイルへの出力が推奨されます。" #: ../../logging.rst:46 +msgid "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`:" +msgstr "" + +#: ../../logging.rst:52 msgid "If you want to setup logging using the library provided configuration without using :meth:`Client.run`, you can use :func:`discord.utils.setup_logging`:" msgstr ":meth:`Client.run` を使用せずにライブラリ提供の構成を使用して logging を設定したい場合は、 :func:`discord.utils.setup_logging` を使用できます。" -#: ../../logging.rst:57 +#: ../../logging.rst:63 msgid "More advanced setups are possible with the :mod:`logging` module. The example below configures a rotating file handler that outputs DEBUG output for everything the library outputs, except for HTTP requests:" msgstr ":mod:`logging` モジュールを使用するとより高度なセットアップが行えます。以下の例では、HTTPリクエスト以外のすべてのライブラリの出力に対しDEBUG出力を使用するローテーションを行うファイルハンドラを構成します。" -#: ../../logging.rst:85 +#: ../../logging.rst:91 msgid "For more information, check the documentation and tutorial of the :mod:`logging` module." msgstr "詳細は、:mod:`logging` モジュールのドキュメントを参照してください。" -#: ../../logging.rst:89 +#: ../../logging.rst:95 msgid "The library now provides a default logging configuration." msgstr "ライブラリがデフォルト logging 構成を提供するようになりました。" diff --git a/docs/locale/ja/LC_MESSAGES/migrating.po b/docs/locale/ja/LC_MESSAGES/migrating.po index 1d60b6898c70..9dad600072ff 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating.po +++ b/docs/locale/ja/LC_MESSAGES/migrating.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/migrating_to_async.po b/docs/locale/ja/LC_MESSAGES/migrating_to_async.po index 250ee432e3cd..cce2227c2a71 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating_to_async.po +++ b/docs/locale/ja/LC_MESSAGES/migrating_to_async.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po b/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po index 4b104b094717..64c88c2cb096 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po +++ b/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/quickstart.po b/docs/locale/ja/LC_MESSAGES/quickstart.po index 4037bde71d88..6befcd7c0e47 100644 --- a/docs/locale/ja/LC_MESSAGES/quickstart.po +++ b/docs/locale/ja/LC_MESSAGES/quickstart.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/sphinx.po b/docs/locale/ja/LC_MESSAGES/sphinx.po index d50bed60dabc..eceded7f8198 100644 --- a/docs/locale/ja/LC_MESSAGES/sphinx.po +++ b/docs/locale/ja/LC_MESSAGES/sphinx.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/version_guarantees.po b/docs/locale/ja/LC_MESSAGES/version_guarantees.po index 3d45729be206..a0e5bc0970c0 100644 --- a/docs/locale/ja/LC_MESSAGES/version_guarantees.po +++ b/docs/locale/ja/LC_MESSAGES/version_guarantees.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/whats_new.po b/docs/locale/ja/LC_MESSAGES/whats_new.po index e849eb8485f7..0a75ffd174a1 100644 --- a/docs/locale/ja/LC_MESSAGES/whats_new.po +++ b/docs/locale/ja/LC_MESSAGES/whats_new.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -26,3407 +26,3471 @@ msgid "This page keeps a detailed human friendly rendering of what's new and cha msgstr "このページでは、特定のバージョンの新機能や変更された機能をわかりやすい形で詳細に記載しています。" #: ../../whats_new.rst:17 +msgid "v2.3.2" +msgstr "" + +#: ../../whats_new.rst:20 +#: ../../whats_new.rst:37 +#: ../../whats_new.rst:91 +#: ../../whats_new.rst:125 +#: ../../whats_new.rst:139 +msgid "Bug Fixes" +msgstr "バグ修正" + +#: ../../whats_new.rst:22 +msgid "Fix the ``name`` parameter not being respected when sending a :class:`CustomActivity`." +msgstr "" + +#: ../../whats_new.rst:23 +msgid "Fix :attr:`Intents.emoji` and :attr:`Intents.emojis_and_stickers` having swapped alias values (:issue:`9471`)." +msgstr "" + +#: ../../whats_new.rst:24 +msgid "Fix ``NameError`` when using :meth:`abc.GuildChannel.create_invite` (:issue:`9505`)." +msgstr "" + +#: ../../whats_new.rst:25 +msgid "Fix crash when disconnecting during the middle of a ``HELLO`` packet when using :class:`AutoShardedClient`." +msgstr "" + +#: ../../whats_new.rst:26 +msgid "Fix overly eager escape behaviour for lists and header markdown in :func:`utils.escape_markdown` (:issue:`9516`)." +msgstr "" + +#: ../../whats_new.rst:27 +msgid "Fix voice websocket not being closed before being replaced by a new one (:issue:`9518`)." +msgstr "" + +#: ../../whats_new.rst:28 +msgid "|commands| Fix the wrong :meth:`~ext.commands.HelpCommand.on_help_command_error` being called when ejected from a cog." +msgstr "" + +#: ../../whats_new.rst:29 +msgid "|commands| Fix ``=None`` being displayed in :attr:`~ext.commands.Command.signature`." +msgstr "" + +#: ../../whats_new.rst:34 +msgid "v2.3.1" +msgstr "" + +#: ../../whats_new.rst:39 +msgid "Fix username lookup in :meth:`Guild.get_member_named` (:issue:`9451`)." +msgstr "" + +#: ../../whats_new.rst:41 +msgid "Use cache data first for :attr:`Interaction.channel` instead of API data." +msgstr "" + +#: ../../whats_new.rst:41 +msgid "This bug usually manifested in incomplete channel objects (e.g. no ``overwrites``) because Discord does not provide this data." +msgstr "" + +#: ../../whats_new.rst:43 +msgid "Fix false positives in :meth:`PartialEmoji.from_str` inappropriately setting ``animated`` to ``True`` (:issue:`9456`, :issue:`9457`)." +msgstr "" + +#: ../../whats_new.rst:44 +msgid "Fix certain select types not appearing in :attr:`Message.components` (:issue:`9462`)." +msgstr "" + +#: ../../whats_new.rst:45 +msgid "|commands| Change lookup order for :class:`~ext.commands.MemberConverter` and :class:`~ext.commands.UserConverter` to prioritise usernames instead of nicknames." +msgstr "" + +#: ../../whats_new.rst:50 msgid "v2.3.0" msgstr "v2.3.0" -#: ../../whats_new.rst:20 -#: ../../whats_new.rst:116 -#: ../../whats_new.rst:206 -#: ../../whats_new.rst:327 -#: ../../whats_new.rst:406 +#: ../../whats_new.rst:53 +#: ../../whats_new.rst:149 +#: ../../whats_new.rst:239 +#: ../../whats_new.rst:360 +#: ../../whats_new.rst:439 msgid "New Features" msgstr "新機能" -#: ../../whats_new.rst:28 +#: ../../whats_new.rst:61 msgid "Add support for the new username system (also known as \"pomelo\")." msgstr "新しいユーザー名システム (\"pomelo\"とも呼ばれます) のサポートを追加しました。" -#: ../../whats_new.rst:23 +#: ../../whats_new.rst:56 msgid "Add :attr:`User.global_name` to get their global nickname or \"display name\"." msgstr "グローバルのニックネーム、つまり「表示名」を取得する :attr:`User.global_name` を追加しました。" -#: ../../whats_new.rst:24 +#: ../../whats_new.rst:57 msgid "Update :attr:`User.display_name` and :attr:`Member.display_name` to understand global nicknames." msgstr ":attr:`User.display_name` と :attr:`Member.display_name` を、グローバルのニックネームを使用するように変更しました。" -#: ../../whats_new.rst:25 +#: ../../whats_new.rst:58 msgid "Update ``__str__`` for :class:`User` to drop discriminators if the user has been migrated." msgstr ":class:`User` の ``__str__`` が、移行したユーザーのタグを含まないよう、変更しました。" -#: ../../whats_new.rst:26 +#: ../../whats_new.rst:59 msgid "Update :meth:`Guild.get_member_named` to work with migrated users." msgstr "移行したユーザーでも動くよう :meth:`Guild.get_member_named` を変更しました。" -#: ../../whats_new.rst:27 +#: ../../whats_new.rst:60 msgid "Update :attr:`User.default_avatar` to work with migrated users." msgstr "移行したユーザーでも動くよう :attr:`User.default_avatar` を変更しました。" -#: ../../whats_new.rst:28 +#: ../../whats_new.rst:61 msgid "|commands| Update user and member converters to understand migrated users." msgstr "|commands| 移行したユーザーを解釈するよう、ユーザーとメンバーコンバータを変更しました。" -#: ../../whats_new.rst:30 +#: ../../whats_new.rst:63 msgid "Add :attr:`DefaultAvatar.pink` for new pink default avatars." msgstr "新しいピンクのデフォルトアバタ―用の :attr:`DefaultAvatar.pink` を追加しました。" -#: ../../whats_new.rst:31 +#: ../../whats_new.rst:64 msgid "Add :meth:`Colour.pink` to get the pink default avatar colour." msgstr "ピンクのデフォルトアバターの色を取得する :meth:`Colour.pink` を追加しました。" -#: ../../whats_new.rst:36 +#: ../../whats_new.rst:69 msgid "Add support for voice messages (:issue:`9358`)" msgstr "ボイスメッセージのサポートを追加しました。 (:issue:`9358`)" -#: ../../whats_new.rst:33 +#: ../../whats_new.rst:66 msgid "Add :attr:`MessageFlags.voice`" msgstr ":attr:`MessageFlags.voice` を追加しました。" -#: ../../whats_new.rst:34 +#: ../../whats_new.rst:67 msgid "Add :attr:`Attachment.duration` and :attr:`Attachment.waveform`" msgstr ":attr:`Attachment.duration` と :attr:`Attachment.waveform` を追加しました。" -#: ../../whats_new.rst:35 +#: ../../whats_new.rst:68 msgid "Add :meth:`Attachment.is_voice_message`" msgstr ":meth:`Attachment.is_voice_message` を追加しました。" -#: ../../whats_new.rst:36 +#: ../../whats_new.rst:69 msgid "This does not support *sending* voice messages because this is currently unsupported by the API." msgstr "ボイスメッセージの *送信* は現在APIで対応していないため、サポートされていません。" -#: ../../whats_new.rst:38 +#: ../../whats_new.rst:71 msgid "Add support for new :attr:`Interaction.channel` attribute from the API update (:issue:`9339`)." msgstr "API更新で追加された :attr:`Interaction.channel` 属性のサポートを追加しました。 (:issue:`9339`)" -#: ../../whats_new.rst:39 +#: ../../whats_new.rst:72 msgid "Add support for :attr:`TextChannel.default_thread_slowmode_delay` (:issue:`9291`)." msgstr ":attr:`TextChannel.default_thread_slowmode_delay` のサポートを追加しました。 (:issue:`9291`)" -#: ../../whats_new.rst:40 +#: ../../whats_new.rst:73 msgid "Add support for :attr:`ForumChannel.default_sort_order` (:issue:`9290`)." msgstr ":attr:`ForumChannel.default_sort_order` のサポートを追加しました。 (:issue:`9290`)" -#: ../../whats_new.rst:41 +#: ../../whats_new.rst:74 msgid "Add support for ``default_reaction_emoji`` and ``default_forum_layout`` in :meth:`Guild.create_forum` (:issue:`9300`)." msgstr ":meth:`Guild.create_forum` にて、 ``default_reaction_emoji`` と ``default_forum_layout`` のサポートを追加しました。 (:issue:`9300`)" -#: ../../whats_new.rst:42 +#: ../../whats_new.rst:75 msgid "Add support for ``widget_channel``, ``widget_enabled``, and ``mfa_level`` in :meth:`Guild.edit` (:issue:`9302`, :issue:`9303`)." msgstr ":meth:`Guild.edit` にて、 ``widget_channel`` 、 ``widget_enabled`` 、 ``mfa_level`` のサポートを追加しました。(:issue:`9302` 、 :issue:`9303`)" -#: ../../whats_new.rst:45 +#: ../../whats_new.rst:78 msgid "Add various new :class:`Permissions` and changes (:issue:`9312`, :issue:`9325`, :issue:`9358`, :issue:`9378`)" msgstr "新しい :class:`Permissions` を追加しました。 (:issue:`9312` 、 :issue:`9325` 、 :issue:`9358` 、 :issue:`9378`)" -#: ../../whats_new.rst:44 +#: ../../whats_new.rst:77 msgid "Add new :attr:`~Permissions.manage_expressions`, :attr:`~Permissions.use_external_sounds`, :attr:`~Permissions.use_soundboard`, :attr:`~Permissions.send_voice_messages`, :attr:`~Permissions.create_expressions` permissions." msgstr "新しい :attr:`~Permissions.manage_expressions` 、 :attr:`~Permissions.use_external_sounds` 、 :attr:`~Permissions.use_soundboard` 、 :attr:`~Permissions.send_voice_messages` 、 :attr:`~Permissions.create_expressions` 権限を追加しました。" -#: ../../whats_new.rst:45 +#: ../../whats_new.rst:78 msgid "Change :attr:`Permissions.manage_emojis` to be an alias of :attr:`~Permissions.manage_expressions`." msgstr ":attr:`Permissions.manage_emojis` を :attr:`~Permissions.manage_expressions` のエイリアスに変更しました。" -#: ../../whats_new.rst:47 +#: ../../whats_new.rst:80 msgid "Add various new properties to :class:`PartialAppInfo` and :class:`AppInfo` (:issue:`9298`)." msgstr ":class:`PartialAppInfo` と :class:`AppInfo` にさまざまな新しいプロパティを追加しました。 (:issue:`9298`)" -#: ../../whats_new.rst:48 +#: ../../whats_new.rst:81 msgid "Add support for ``with_counts`` parameter to :meth:`Client.fetch_guilds` (:issue:`9369`)." msgstr ":meth:`Client.fetch_guilds` に ``with_counts`` 引数のサポートを追加しました。 (:issue:`9369`)" -#: ../../whats_new.rst:49 +#: ../../whats_new.rst:82 msgid "Add new :meth:`Guild.get_emoji` helper (:issue:`9296`)." msgstr "新しく :meth:`Guild.get_emoji` ヘルパーを追加しました。 (:issue:`9296`)" -#: ../../whats_new.rst:50 +#: ../../whats_new.rst:83 msgid "Add :attr:`ApplicationFlags.auto_mod_badge` (:issue:`9313`)." msgstr ":attr:`ApplicationFlags.auto_mod_badge` を追加しました。 (:issue:`9313`)" -#: ../../whats_new.rst:51 +#: ../../whats_new.rst:84 msgid "Add :attr:`Guild.max_stage_video_users` and :attr:`Guild.safety_alerts_channel` (:issue:`9318`)." msgstr ":attr:`Guild.max_stage_video_users` と :attr:`Guild.safety_alerts_channel` を追加しました。 (:issue:`9318`)" -#: ../../whats_new.rst:52 +#: ../../whats_new.rst:85 msgid "Add support for ``raid_alerts_disabled`` and ``safety_alerts_channel`` in :meth:`Guild.edit` (:issue:`9318`)." msgstr ":meth:`Guild.edit` にて ``raid_alerts_disabled`` と ``safety_alerts_channel`` のサポートを追加しました。 (:issue:`9318`)" -#: ../../whats_new.rst:53 +#: ../../whats_new.rst:86 msgid "|commands| Add :attr:`BadLiteralArgument.argument ` to get the failed argument's value (:issue:`9283`)." msgstr "|commands| 失敗した引数の値を取得するための :attr:`BadLiteralArgument.argument ` を追加しました。 (:issue:`9283`)" -#: ../../whats_new.rst:54 +#: ../../whats_new.rst:87 msgid "|commands| Add :attr:`Context.filesize_limit ` property (:issue:`9416`)." msgstr "|commands| :attr:`Context.filesize_limit ` 属性を追加しました。 (:issue:`9416`)" -#: ../../whats_new.rst:55 +#: ../../whats_new.rst:88 msgid "|commands| Add support for :attr:`Parameter.displayed_name ` (:issue:`9427`)." msgstr "|commands| :attr:`Parameter.displayed_name ` のサポートを追加しました。 (:issue:`9427`)" -#: ../../whats_new.rst:58 -#: ../../whats_new.rst:92 -#: ../../whats_new.rst:106 -#: ../../whats_new.rst:159 -#: ../../whats_new.rst:196 -msgid "Bug Fixes" -msgstr "バグ修正" - -#: ../../whats_new.rst:61 +#: ../../whats_new.rst:94 msgid "Fix ``FileHandler`` handlers being written ANSI characters when the bot is executed inside PyCharm." msgstr "PyCharm 内でボットが実行された場合、 ``FileHandler`` ハンドラにANSI 文字が出力されるのを修正しました。" -#: ../../whats_new.rst:61 +#: ../../whats_new.rst:94 msgid "This has the side effect of removing coloured logs from the PyCharm terminal due an upstream bug involving TTY detection. This issue is tracked under `PY-43798 `_." msgstr "PyCharmのTTY検出のバグの影響により、PyCharm ターミナル内でログに色が付かなくなる副作用があります。このバグは `PY-43798 `_ で追跡されています。" -#: ../../whats_new.rst:63 +#: ../../whats_new.rst:96 msgid "Fix channel edits with :meth:`Webhook.edit` sending two requests instead of one." msgstr ":meth:`Webhook.edit` でチャンネルを編集するときに2回リクエストが行われるバグを修正しました。" -#: ../../whats_new.rst:64 +#: ../../whats_new.rst:97 msgid "Fix :attr:`StageChannel.last_message_id` always being ``None`` (:issue:`9422`)." msgstr ":attr:`StageChannel.last_message_id` が常に ``None`` となるのを修正しました。 (:issue:`9422`)" -#: ../../whats_new.rst:65 +#: ../../whats_new.rst:98 msgid "Fix piped audio input ending prematurely (:issue:`9001`, :issue:`9380`)." msgstr "パイプによるオーディオ入力が終了するのが早すぎる問題を修正しました。 (:issue:`9001` 、 :issue:`9380`)" -#: ../../whats_new.rst:66 +#: ../../whats_new.rst:99 msgid "Fix persistent detection for :class:`ui.TextInput` being incorrect if the ``custom_id`` is set later (:issue:`9438`)." msgstr "``custom_id`` が後で設定された場合、 :class:`ui.TextInput` の永続的な検出が正しくない問題を修正しました。 (:issue:`9438`)" -#: ../../whats_new.rst:67 +#: ../../whats_new.rst:100 msgid "Fix custom attributes not being copied over when inheriting from :class:`app_commands.Group` (:issue:`9383`)." msgstr ":class:`app_commands.Group` から継承するときにカスタム属性がコピーされない問題を修正しました。 (:issue:`9383`)" -#: ../../whats_new.rst:68 +#: ../../whats_new.rst:101 msgid "Fix AutoMod audit log entry error due to empty channel_id (:issue:`9384`)." msgstr "空の channel_id により自動管理の監査ログ項目でエラーが発生するのを修正しました。 (:issue:`9384`)" -#: ../../whats_new.rst:69 +#: ../../whats_new.rst:102 msgid "Fix handling of ``around`` parameter in :meth:`abc.Messageable.history` (:issue:`9388`)." msgstr ":meth:`abc.Messageable.history` の ``around`` 引数の扱いを修正しました。 (:issue:`9388`)" -#: ../../whats_new.rst:70 +#: ../../whats_new.rst:103 msgid "Fix occasional :exc:`AttributeError` when accessing the :attr:`ClientUser.mutual_guilds` property (:issue:`9387`)." msgstr ":attr:`ClientUser.mutual_guilds` プロパティにアクセスするとき時々 :exc:`AttributeError` が発生する問題を修正しました。 (:issue:`9387`)" -#: ../../whats_new.rst:71 +#: ../../whats_new.rst:104 msgid "Fix :func:`utils.escape_markdown` not escaping the new markdown (:issue:`9361`)." msgstr ":func:`utils.escape_markdown` が新しいマークダウンを正しくエスケープしない問題を修正しました。 (:issue:`9361`)" -#: ../../whats_new.rst:72 +#: ../../whats_new.rst:105 msgid "Fix webhook targets not being converted in audit logs (:issue:`9332`)." msgstr "監査ログでWebhookターゲットが変換されない問題を修正しました。 (:issue:`9332`)" -#: ../../whats_new.rst:73 +#: ../../whats_new.rst:106 msgid "Fix error when not passing ``enabled`` in :meth:`Guild.create_automod_rule` (:issue:`9292`)." msgstr ":meth:`Guild.create_automod_rule` で ``enabled`` を渡さないときに生じるエラーを修正しました。 (:issue:`9292`)" -#: ../../whats_new.rst:74 +#: ../../whats_new.rst:107 msgid "Fix how various parameters are handled in :meth:`Guild.create_scheduled_event` (:issue:`9275`)." msgstr ":meth:`Guild.create_scheduled_event` のパラメータの扱いを修正しました。 (:issue:`9275`)" -#: ../../whats_new.rst:75 +#: ../../whats_new.rst:108 msgid "Fix not sending the ``ssrc`` parameter when sending the SPEAKING payload (:issue:`9301`)." msgstr "SPEAKING ペイロードの送信時に ``ssrc`` パラメータを送信しない問題を修正しました。 (:issue:`9301`)" -#: ../../whats_new.rst:76 +#: ../../whats_new.rst:109 msgid "Fix :attr:`Message.guild` being ``None`` sometimes when received via an interaction." msgstr "インタラクションで受け取った :attr:`Message.guild` が時々 ``None`` になる問題を修正しました。" -#: ../../whats_new.rst:77 +#: ../../whats_new.rst:110 msgid "Fix :attr:`Message.system_content` for :attr:`MessageType.channel_icon_change` (:issue:`9410`)." msgstr ":attr:`MessageType.channel_icon_change` の :attr:`Message.system_content` を修正しました。 (:issue:`9410`)" -#: ../../whats_new.rst:80 -#: ../../whats_new.rst:180 -#: ../../whats_new.rst:250 -#: ../../whats_new.rst:392 -#: ../../whats_new.rst:459 +#: ../../whats_new.rst:113 +#: ../../whats_new.rst:213 +#: ../../whats_new.rst:283 +#: ../../whats_new.rst:425 +#: ../../whats_new.rst:492 msgid "Miscellaneous" msgstr "その他" -#: ../../whats_new.rst:82 +#: ../../whats_new.rst:115 msgid "Update the base :attr:`Guild.filesize_limit` to 25MiB (:issue:`9353`)." msgstr "基本の :attr:`Guild.filesize_limit` を 25MiB に更新しました。 (:issue:`9353`)" -#: ../../whats_new.rst:83 +#: ../../whats_new.rst:116 msgid "Allow Interaction webhook URLs to be used in :meth:`Webhook.from_url`." msgstr ":meth:`Webhook.from_url` でインタラクション Webhook URLを使用できるようになりました。" -#: ../../whats_new.rst:84 +#: ../../whats_new.rst:117 msgid "Set the socket family of internal connector to ``AF_INET`` to prevent IPv6 connections (:issue:`9442`, :issue:`9443`)." msgstr "IPv6 接続を防ぐために、内部コネクタのソケットファミリを ``AF_INET`` に設定するようにしました。 (:issue:`9442` 、 :issue:`9443`)" -#: ../../whats_new.rst:89 +#: ../../whats_new.rst:122 msgid "v2.2.3" msgstr "v2.2.3" -#: ../../whats_new.rst:94 +#: ../../whats_new.rst:127 msgid "Fix crash from Discord sending null ``channel_id`` for automod audit logs." msgstr "Discordが自動管理の監査ログに関し null の ``channel_id`` を送ることによって生じたクラッシュを修正しました。" -#: ../../whats_new.rst:95 +#: ../../whats_new.rst:128 msgid "Fix ``channel`` edits when using :meth:`Webhook.edit` sending two requests." msgstr ":meth:`Webhook.edit` を使用して ``channel`` を変更するときに2回リクエストが送信されるバグを修正しました。" -#: ../../whats_new.rst:96 +#: ../../whats_new.rst:129 msgid "Fix :attr:`AuditLogEntry.target` being ``None`` for invites (:issue:`9336`)." msgstr "招待に関し :attr:`AuditLogEntry.target` が ``None`` となるのを修正しました。 (:issue:`9336`)" -#: ../../whats_new.rst:97 +#: ../../whats_new.rst:130 msgid "Fix :exc:`KeyError` when accessing data for :class:`GuildSticker` (:issue:`9324`)." msgstr ":class:`GuildSticker` のデータにアクセスするときの :exc:`KeyError` を修正しました。 (:issue:`9324`)" -#: ../../whats_new.rst:103 +#: ../../whats_new.rst:136 msgid "v2.2.2" msgstr "v2.2.2" -#: ../../whats_new.rst:108 +#: ../../whats_new.rst:141 msgid "Fix UDP discovery in voice not using new 74 byte layout which caused voice to break (:issue:`9277`, :issue:`9278`)" msgstr "ボイスのUDP検出が、新しい74バイトレイアウトを使用していないため、ボイスが使用できない問題を修正しました。 (:issue:`9277` 、 :issue:`9278`)" -#: ../../whats_new.rst:113 +#: ../../whats_new.rst:146 msgid "v2.2.0" msgstr "v2.2.0" -#: ../../whats_new.rst:118 +#: ../../whats_new.rst:151 msgid "Add support for new :func:`on_audit_log_entry_create` event" msgstr "新しい :func:`on_audit_log_entry_create` イベントのサポートを追加しました。" -#: ../../whats_new.rst:120 +#: ../../whats_new.rst:153 msgid "Add support for silent messages via ``silent`` parameter in :meth:`abc.Messageable.send`" msgstr "サイレントメッセージを送信する :meth:`abc.Messageable.send` の ``silent`` パラメータのサポートを追加しました。" -#: ../../whats_new.rst:120 +#: ../../whats_new.rst:153 msgid "This is queryable via :attr:`MessageFlags.suppress_notifications`" msgstr "これは :attr:`MessageFlags.suppress_notifications` から確認できます。" -#: ../../whats_new.rst:122 +#: ../../whats_new.rst:155 msgid "Implement :class:`abc.Messageable` for :class:`StageChannel` (:issue:`9248`)" msgstr ":class:`StageChannel` が :class:`abc.Messageable` を実装するようにしました。 (:issue:`9248`)" -#: ../../whats_new.rst:123 +#: ../../whats_new.rst:156 msgid "Add setter for :attr:`discord.ui.ChannelSelect.channel_types` (:issue:`9068`)" msgstr ":attr:`discord.ui.ChannelSelect.channel_types` のセッターを追加しました。 (:issue:`9068`)" -#: ../../whats_new.rst:124 +#: ../../whats_new.rst:157 msgid "Add support for custom messages in automod via :attr:`AutoModRuleAction.custom_message` (:issue:`9267`)" msgstr ":attr:`AutoModRuleAction.custom_message` で、自動管理のカスタムメッセージのサポートを追加しました。 (:issue:`9267`)" -#: ../../whats_new.rst:125 +#: ../../whats_new.rst:158 msgid "Add :meth:`ForumChannel.get_thread` (:issue:`9106`)" msgstr ":meth:`ForumChannel.get_thread` を追加しました。 (:issue:`9106`)" -#: ../../whats_new.rst:126 +#: ../../whats_new.rst:159 msgid "Add :attr:`StageChannel.slowmode_delay` and :attr:`VoiceChannel.slowmode_delay` (:issue:`9111`)" msgstr ":attr:`StageChannel.slowmode_delay` と :attr:`VoiceChannel.slowmode_delay` を追加しました。 (:issue:`9111`)" -#: ../../whats_new.rst:127 +#: ../../whats_new.rst:160 msgid "Add support for editing the slowmode for :class:`StageChannel` and :class:`VoiceChannel` (:issue:`9111`)" msgstr ":class:`StageChannel` と :class:`VoiceChannel` の低速モードの変更のサポートを追加しました。 (:issue:`9111`)" -#: ../../whats_new.rst:128 +#: ../../whats_new.rst:161 msgid "Add :attr:`Locale.indonesian`" msgstr ":attr:`Locale.indonesian` を追加しました。" -#: ../../whats_new.rst:129 +#: ../../whats_new.rst:162 msgid "Add ``delete_after`` keyword argument to :meth:`Interaction.edit_message` (:issue:`9415`)" msgstr ":meth:`Interaction.edit_message` に ``delete_after`` キーワード引数を追加しました。 (:issue:`9415`)" -#: ../../whats_new.rst:130 +#: ../../whats_new.rst:163 msgid "Add ``delete_after`` keyword argument to :meth:`InteractionMessage.edit` (:issue:`9206`)" msgstr ":meth:`InteractionMessage.edit` に ``delete_after`` キーワード引数を追加しました。 (:issue:`9206`)" -#: ../../whats_new.rst:133 +#: ../../whats_new.rst:166 msgid "Add support for member flags (:issue:`9204`)" msgstr "メンバーフラグのサポートを追加しました。 (:issue:`9204`)" -#: ../../whats_new.rst:132 +#: ../../whats_new.rst:165 msgid "Accessible via :attr:`Member.flags` and has a type of :class:`MemberFlags`" msgstr ":attr:`Member.flags` でアクセスでき、型は :class:`MemberFlags` です。" -#: ../../whats_new.rst:133 +#: ../../whats_new.rst:166 msgid "Support ``bypass_verification`` within :meth:`Member.edit`" msgstr ":meth:`Member.edit` にて ``bypass_verification`` のサポートを追加しました。" -#: ../../whats_new.rst:136 +#: ../../whats_new.rst:169 msgid "Add support for passing a client to :meth:`Webhook.from_url` and :meth:`Webhook.partial`" msgstr ":meth:`Webhook.from_url` と :meth:`Webhook.partial` にクライアントを渡せるようにしました。" -#: ../../whats_new.rst:136 +#: ../../whats_new.rst:169 msgid "This allows them to use views (assuming they are \"bot owned\" webhooks)" msgstr "これにより、ビューを使用することができます (「ボット所有」Webhookである場合は)。" -#: ../../whats_new.rst:138 +#: ../../whats_new.rst:171 msgid "Add :meth:`Colour.dark_embed` and :meth:`Colour.light_embed` (:issue:`9219`)" msgstr ":meth:`Colour.dark_embed` と :meth:`Colour.light_embed` を追加しました。 (:issue:`9219`)" -#: ../../whats_new.rst:139 +#: ../../whats_new.rst:172 msgid "Add support for many more parameters within :meth:`Guild.create_stage_channel` (:issue:`9245`)" msgstr ":meth:`Guild.create_stage_channel` で対応するパラメータを追加しました。 (:issue:`9245`)" -#: ../../whats_new.rst:140 +#: ../../whats_new.rst:173 msgid "Add :attr:`AppInfo.role_connections_verification_url`" msgstr ":attr:`AppInfo.role_connections_verification_url` を追加しました。" -#: ../../whats_new.rst:141 +#: ../../whats_new.rst:174 msgid "Add support for :attr:`ForumChannel.default_layout`" msgstr ":attr:`ForumChannel.default_layout` のサポートを追加しました。" -#: ../../whats_new.rst:142 +#: ../../whats_new.rst:175 msgid "Add various new :class:`MessageType` values such as ones related to stage channel and role subscriptions" msgstr "ステージチャンネルやロールサブスクリプションに関連するものなど、新しい :class:`MessageType` 値を追加しました。" -#: ../../whats_new.rst:149 +#: ../../whats_new.rst:182 msgid "Add support for role subscription related attributes" msgstr "ロールサブスクリプション関連属性のサポートを追加しました。" -#: ../../whats_new.rst:144 +#: ../../whats_new.rst:177 msgid ":class:`RoleSubscriptionInfo` within :attr:`Message.role_subscription`" msgstr ":attr:`Message.role_subscription` と :class:`RoleSubscriptionInfo` 。" -#: ../../whats_new.rst:145 +#: ../../whats_new.rst:178 msgid ":attr:`MessageType.role_subscription_purchase`" msgstr ":attr:`MessageType.role_subscription_purchase`" -#: ../../whats_new.rst:146 +#: ../../whats_new.rst:179 msgid ":attr:`SystemChannelFlags.role_subscription_purchase_notifications`" msgstr ":attr:`SystemChannelFlags.role_subscription_purchase_notifications`" -#: ../../whats_new.rst:147 +#: ../../whats_new.rst:180 msgid ":attr:`SystemChannelFlags.role_subscription_purchase_notification_replies`" msgstr ":attr:`SystemChannelFlags.role_subscription_purchase_notification_replies`" -#: ../../whats_new.rst:148 +#: ../../whats_new.rst:181 msgid ":attr:`RoleTags.subscription_listing_id`" msgstr ":attr:`RoleTags.subscription_listing_id`" -#: ../../whats_new.rst:149 +#: ../../whats_new.rst:182 msgid ":meth:`RoleTags.is_available_for_purchase`" msgstr ":meth:`RoleTags.is_available_for_purchase`" -#: ../../whats_new.rst:151 +#: ../../whats_new.rst:184 msgid "Add support for checking if a role is a linked role under :meth:`RoleTags.is_guild_connection`" msgstr ":meth:`RoleTags.is_guild_connection` で、ロールが紐づいたロールかの確認のサポートを追加しました。" -#: ../../whats_new.rst:152 +#: ../../whats_new.rst:185 msgid "Add support for GIF sticker type" msgstr "GIFスタンプタイプのサポートを追加しました。" -#: ../../whats_new.rst:153 +#: ../../whats_new.rst:186 msgid "Add support for :attr:`Message.application_id` and :attr:`Message.position`" msgstr ":attr:`Message.application_id` と :attr:`Message.position` のサポートを追加しました。" -#: ../../whats_new.rst:154 +#: ../../whats_new.rst:187 msgid "Add :func:`utils.maybe_coroutine` helper" msgstr ":func:`utils.maybe_coroutine` ヘルパーを追加しました。" -#: ../../whats_new.rst:155 +#: ../../whats_new.rst:188 msgid "Add :attr:`ScheduledEvent.creator_id` attribute" msgstr ":attr:`ScheduledEvent.creator_id` 属性を追加しました。" -#: ../../whats_new.rst:156 +#: ../../whats_new.rst:189 msgid "|commands| Add support for :meth:`~ext.commands.Cog.interaction_check` for :class:`~ext.commands.GroupCog` (:issue:`9189`)" msgstr "|commands| :class:`~ext.commands.GroupCog` にて :meth:`~ext.commands.Cog.interaction_check` のサポートを追加しました。 (:issue:`9189`)" -#: ../../whats_new.rst:161 +#: ../../whats_new.rst:194 msgid "Fix views not being removed from message store backing leading to a memory leak when used from an application command context" msgstr "アプリケーションコマンドから使用されたビューがメッセージストアから除去されず、メモリリークを引き起こすバグを修正しました。" -#: ../../whats_new.rst:162 +#: ../../whats_new.rst:195 msgid "Fix async iterators requesting past their bounds when using ``oldest_first`` and ``after`` or ``before`` (:issue:`9093`)" msgstr "非同期イテレータが ``oldest_first`` と ``after`` または ``before`` を指定した場合に境界を越えてリクエストをするのを修正しました。 (:issue:`9093`)" -#: ../../whats_new.rst:163 +#: ../../whats_new.rst:196 msgid "Fix :meth:`Guild.audit_logs` pagination logic being buggy when using ``after`` (:issue:`9269`)" msgstr ":meth:`Guild.audit_logs` にて、 ``after`` を使用したときにページネーションで発生するバグを修正しました。 (:issue:`9269`)" -#: ../../whats_new.rst:164 +#: ../../whats_new.rst:197 msgid "Fix :attr:`Message.channel` sometimes being :class:`Object` instead of :class:`PartialMessageable`" msgstr ":attr:`Message.channel` が時々 :class:`PartialMessageable` ではなく :class:`Object` となるバグを修正しました。" -#: ../../whats_new.rst:165 +#: ../../whats_new.rst:198 msgid "Fix :class:`ui.View` not properly calling ``super().__init_subclass__`` (:issue:`9231`)" msgstr ":class:`ui.View` が ``super().__init_subclass__`` を適切に呼び出さないのを修正しました。 (:issue:`9231`)" -#: ../../whats_new.rst:166 +#: ../../whats_new.rst:199 msgid "Fix ``available_tags`` and ``default_thread_slowmode_delay`` not being respected in :meth:`Guild.create_forum`" msgstr ":meth:`Guild.create_forum` で渡された ``available_tags`` と ``default_thread_slowmode_delay`` が使用されない問題を修正しました。" -#: ../../whats_new.rst:167 +#: ../../whats_new.rst:200 msgid "Fix :class:`AutoModTrigger` ignoring ``allow_list`` with type keyword (:issue:`9107`)" msgstr ":class:`AutoModTrigger` が type キーワードのある ``allow_list`` を無視するバグを修正しました。 (:issue:`9107`)" -#: ../../whats_new.rst:168 +#: ../../whats_new.rst:201 msgid "Fix implicit permission resolution for :class:`Thread` (:issue:`9153`)" msgstr ":class:`Thread` の暗黙的な権限の解決を修正しました。 (:issue:`9153`)" -#: ../../whats_new.rst:169 +#: ../../whats_new.rst:202 msgid "Fix :meth:`AutoModRule.edit` to work with actual snowflake types such as :class:`Object` (:issue:`9159`)" msgstr ":meth:`AutoModRule.edit` を、 :class:`Object` のようなスノウフレーク型で動くよう修正しました。 (:issue:`9159`)" -#: ../../whats_new.rst:170 +#: ../../whats_new.rst:203 msgid "Fix :meth:`Webhook.send` returning :class:`ForumChannel` for :attr:`WebhookMessage.channel`" msgstr ":meth:`Webhook.send` が :attr:`WebhookMessage.channel` に関し :class:`ForumChannel` を返すのを修正しました。" -#: ../../whats_new.rst:171 +#: ../../whats_new.rst:204 msgid "When a lookup for :attr:`AuditLogEntry.target` fails, it will fallback to :class:`Object` with the appropriate :attr:`Object.type` (:issue:`9171`)" msgstr ":attr:`AuditLogEntry.target` の検索が失敗したとき、適切な :attr:`Object.type` をもつ :class:`Object` にフォールバックするようにしました。 (:issue:`9171`)" -#: ../../whats_new.rst:172 +#: ../../whats_new.rst:205 msgid "Fix :attr:`AuditLogDiff.type` for integrations returning :class:`ChannelType` instead of :class:`str` (:issue:`9200`)" msgstr "インテグレーションの :attr:`AuditLogDiff.type` が :class:`str` ではなく :class:`ChannelType` を返すのを修正しました。 (:issue:`9200`)" -#: ../../whats_new.rst:173 +#: ../../whats_new.rst:206 msgid "Fix :attr:`AuditLogDiff.type` for webhooks returning :class:`ChannelType` instead of :class:`WebhookType` (:issue:`9251`)" msgstr "Webhookの :attr:`AuditLogDiff.type` が :class:`WebhookType` ではなく :class:`ChannelType` を返すのを修正しました。 (:issue:`9251`)" -#: ../../whats_new.rst:174 +#: ../../whats_new.rst:207 msgid "Fix webhooks and interactions not properly closing files after the request has completed" msgstr "Webhookとインタラクションが、リクエストが完了した後にファイルを正しく閉じないバグを修正しました。" -#: ../../whats_new.rst:175 +#: ../../whats_new.rst:208 msgid "Fix :exc:`NameError` in audit log target for app commands" msgstr "アプリケーションコマンドの監査ログターゲットでの :exc:`NameError` を修正しました。" -#: ../../whats_new.rst:176 +#: ../../whats_new.rst:209 msgid "Fix :meth:`ScheduledEvent.edit` requiring some arguments to be passed in when unnecessary (:issue:`9261`, :issue:`9268`)" msgstr ":meth:`ScheduledEvent.edit` にて不必要な引数が必須とされるバグを修正しました。 (:issue:`9261` 、 :issue:`9268`)" -#: ../../whats_new.rst:177 +#: ../../whats_new.rst:210 msgid "|commands| Explicit set a traceback for hybrid command invocations (:issue:`9205`)" msgstr "|commands| ハイブリッドコマンドを呼び出すとき、明示的にトレースバックを設定するようにしました。 (:issue:`9205`)" -#: ../../whats_new.rst:182 +#: ../../whats_new.rst:215 msgid "Add colour preview for the colours predefined in :class:`Colour`" msgstr ":class:`Colour` で定義された色のプレビューを追加しました。" -#: ../../whats_new.rst:183 +#: ../../whats_new.rst:216 msgid "Finished views are no longer stored by the library when sending them (:issue:`9235`)" msgstr "終了したビューは送信時にライブラリで保管されないようになりました。 (:issue:`9235`)" -#: ../../whats_new.rst:184 +#: ../../whats_new.rst:217 msgid "Force enable colour logging for the default logging handler when run under Docker." msgstr "Docker下で実行するときに、デフォルトの logging ハンドラで色のついたログを常に有効にするようにしました。" -#: ../../whats_new.rst:185 +#: ../../whats_new.rst:218 msgid "Add various overloads for :meth:`Client.wait_for` to aid in static analysis (:issue:`9184`)" msgstr "静的解析のために、 :meth:`Client.wait_for` のオーバーロードを追加しました。 (:issue:`9184`)" -#: ../../whats_new.rst:186 +#: ../../whats_new.rst:219 msgid ":class:`Interaction` can now optionally take a generic parameter, ``ClientT`` to represent the type for :attr:`Interaction.client`" msgstr ":class:`Interaction` は、オプションでジェネリックのパラメータ ``ClientT`` をとり、 :attr:`Interaction.client` の型を指定できるようになりました。" -#: ../../whats_new.rst:187 +#: ../../whats_new.rst:220 msgid "|commands| Respect :attr:`~ext.commands.Command.ignore_extra` for :class:`~discord.ext.commands.FlagConverter` keyword-only parameters" msgstr "|commands| :class:`~discord.ext.commands.FlagConverter` キーワードのみのパラメータでも、 :attr:`~ext.commands.Command.ignore_extra` に従うようにしました。" -#: ../../whats_new.rst:188 +#: ../../whats_new.rst:221 msgid "|commands| Change :attr:`Paginator.pages ` to not prematurely close (:issue:`9257`)" msgstr "|commands| :attr:`Paginator.pages ` を早期に閉じないように変更しました。 (:issue:`9257`)" -#: ../../whats_new.rst:193 +#: ../../whats_new.rst:226 msgid "v2.1.1" msgstr "v2.1.1" -#: ../../whats_new.rst:198 +#: ../../whats_new.rst:231 msgid "Fix crash involving GIF stickers when looking up their filename extension." msgstr "GIF スタンプのファイル名の拡張子を検索するときのクラッシュを修正しました。" -#: ../../whats_new.rst:203 +#: ../../whats_new.rst:236 msgid "v2.1.0" msgstr "v2.1.0" -#: ../../whats_new.rst:208 +#: ../../whats_new.rst:241 msgid "Add support for ``delete_message_seconds`` in :meth:`Guild.ban` (:issue:`8391`)" msgstr ":meth:`Guild.ban` に ``delete_message_seconds`` へのサポートを追加しました。 (:issue:`8391`)" -#: ../../whats_new.rst:209 +#: ../../whats_new.rst:242 msgid "Add support for automod related audit log actions (:issue:`8389`)" msgstr "AutoMod関連の監査ログアクションのサポートを追加しました。 (:issue:`8389`)" -#: ../../whats_new.rst:210 +#: ../../whats_new.rst:243 msgid "Add support for :class:`ForumChannel` annotations in app commands" msgstr "アプリケーションコマンドで :class:`ForumChannel` アノテーションのサポートを追加しました。" -#: ../../whats_new.rst:211 +#: ../../whats_new.rst:244 msgid "Add support for :attr:`ForumChannel.default_thread_slowmode_delay`." msgstr ":attr:`ForumChannel.default_thread_slowmode_delay` のサポートを追加しました。" -#: ../../whats_new.rst:212 +#: ../../whats_new.rst:245 msgid "Add support for :attr:`ForumChannel.default_reaction_emoji`." msgstr ":attr:`ForumChannel.default_reaction_emoji` のサポートを追加しました。" -#: ../../whats_new.rst:215 +#: ../../whats_new.rst:248 msgid "Add support for forum tags under :class:`ForumTag`." msgstr ":class:`ForumTag` にて、フォーラムタグのサポートを追加しました。" -#: ../../whats_new.rst:214 +#: ../../whats_new.rst:247 msgid "Tags can be obtained using :attr:`ForumChannel.available_tags` or :meth:`ForumChannel.get_tag`." msgstr "タグは :attr:`ForumChannel.available_tags` または :meth:`ForumChannel.get_tag` で取得できます。" -#: ../../whats_new.rst:215 +#: ../../whats_new.rst:248 msgid "See :meth:`Thread.edit` and :meth:`ForumChannel.edit` for modifying tags and their usage." msgstr "タグの変更方法や使い方については :meth:`Thread.edit` と :meth:`ForumChannel.edit` を参照してください。" -#: ../../whats_new.rst:219 +#: ../../whats_new.rst:252 msgid "Add support for new select types (:issue:`9013`, :issue:`9003`)." msgstr "新しい選択メニューの種類のサポートを追加しました。 (:issue:`9013`, :issue:`9003`)" -#: ../../whats_new.rst:218 +#: ../../whats_new.rst:251 msgid "These are split into separate classes, :class:`~discord.ui.ChannelSelect`, :class:`~discord.ui.RoleSelect`, :class:`~discord.ui.UserSelect`, :class:`~discord.ui.MentionableSelect`." msgstr "これらは、 :class:`~discord.ui.ChannelSelect` 、 :class:`~discord.ui.RoleSelect` 、 :class:`~discord.ui.UserSelect` 、 :class:`~discord.ui.MentionableSelect` に分割されています。" -#: ../../whats_new.rst:219 +#: ../../whats_new.rst:252 msgid "The decorator still uses a single function, :meth:`~discord.ui.select`. Changing the select type is done by the ``cls`` keyword parameter." msgstr "デコレータはこれまで通り単一の関数 :meth:`~discord.ui.select` を使用しています。選択メニューの種類の変更は ``cls`` キーワード引数によって行われます。" -#: ../../whats_new.rst:221 +#: ../../whats_new.rst:254 msgid "Add support for toggling discoverable and invites_disabled features in :meth:`Guild.edit` (:issue:`8390`)." msgstr ":meth:`Guild.edit` で、discoverable と invites_disabled 機能を切り替えるためのサポートを追加しました。 (:issue:`8390`)" -#: ../../whats_new.rst:222 +#: ../../whats_new.rst:255 msgid "Add :meth:`Interaction.translate` helper method (:issue:`8425`)." msgstr ":meth:`Interaction.translate` ヘルパーメソッドを追加しました。 (:issue:`8425`)" -#: ../../whats_new.rst:223 +#: ../../whats_new.rst:256 msgid "Add :meth:`Forum.archived_threads` (:issue:`8476`)." msgstr ":meth:`Forum.archived_threads` を追加しました。 (:issue:`8476`)" -#: ../../whats_new.rst:224 +#: ../../whats_new.rst:257 msgid "Add :attr:`ApplicationFlags.active`, :attr:`UserFlags.active_developer`, and :attr:`PublicUserFlags.active_developer`." msgstr ":attr:`ApplicationFlags.active` 、 :attr:`UserFlags.active_developer` 、および :attr:`PublicUserFlags.active_developer` を追加しました。" -#: ../../whats_new.rst:225 +#: ../../whats_new.rst:258 msgid "Add ``delete_after`` to :meth:`InteractionResponse.send_message` (:issue:`9022`)." msgstr ":meth:`InteractionResponse.send_message` に ``delete_after`` を追加しました。 (:issue:`9022`)" -#: ../../whats_new.rst:226 +#: ../../whats_new.rst:259 msgid "Add support for :attr:`AutoModTrigger.regex_patterns`." msgstr ":attr:`AutoModTrigger.regex_patterns` のサポートを追加しました。" -#: ../../whats_new.rst:227 +#: ../../whats_new.rst:260 msgid "|commands| Add :attr:`GroupCog.group_extras ` to set :attr:`app_commands.Group.extras` (:issue:`8405`)." msgstr "|commands| :attr:`app_commands.Group.extras` を設定できる :attr:`GroupCog.group_extras ` を追加しました。 (:issue:`8405`)" -#: ../../whats_new.rst:228 +#: ../../whats_new.rst:261 msgid "|commands| Add support for NumPy style docstrings for regular commands to set parameter descriptions." msgstr "|commands| 通常のコマンドでパラメータの説明を設定するのに NumPy スタイルの docstring が利用できるようになりました。" -#: ../../whats_new.rst:229 +#: ../../whats_new.rst:262 msgid "|commands| Allow :class:`~discord.ext.commands.Greedy` to potentially maintain state between calls." msgstr "|commands| :class:`~discord.ext.commands.Greedy` が呼び出し間で状態を維持できるようにしました。" -#: ../../whats_new.rst:230 +#: ../../whats_new.rst:263 msgid "|commands| Add :meth:`Cog.has_app_command_error_handler ` (:issue:`8991`)." msgstr "|commands| :meth:`Cog.has_app_command_error_handler ` を追加しました。 (:issue:`8991`)" -#: ../../whats_new.rst:231 +#: ../../whats_new.rst:264 msgid "|commands| Allow ``delete_after`` in :meth:`Context.send ` on ephemeral messages (:issue:`9021`)." msgstr "|commands| :meth:`Context.send ` で ``delete_after`` が利用できるようになりました。 (:issue:`9021`)" -#: ../../whats_new.rst:236 +#: ../../whats_new.rst:269 msgid "Fix an :exc:`KeyError` being raised when constructing :class:`app_commands.Group` with no module (:issue:`8411`)." msgstr ":exc:`app_commands.Group` をモジュールなしで構築したときに :class:`KeyError` が発生する問題を修正しました。 (:issue:`8411`)" -#: ../../whats_new.rst:237 +#: ../../whats_new.rst:270 msgid "Fix unescaped period in webhook URL regex (:issue:`8443`)." msgstr "Webhook URLの正規表現でピリオドがエスケープされていない問題を修正しました。 (:issue:`8443`)" -#: ../../whats_new.rst:238 +#: ../../whats_new.rst:271 msgid "Fix :exc:`app_commands.CommandSyncFailure` raising for other 400 status code errors." msgstr "他の400ステータスコードエラーで :exc:`app_commands.CommandSyncFailure` が送出される問題を修正しました。" -#: ../../whats_new.rst:239 +#: ../../whats_new.rst:272 msgid "Fix potential formatting issues showing `_errors` in :exc:`app_commands.CommandSyncFailure`." msgstr ":exc:`app_commands.CommandSyncFailure` で ``_errors`` を表示するかもしれないフォーマットの問題を修正しました。" -#: ../../whats_new.rst:240 +#: ../../whats_new.rst:273 msgid "Fix :attr:`Guild.stage_instances` and :attr:`Guild.schedule_events` clearing on ``GUILD_UPDATE``." msgstr ":attr:`Guild.stage_instances` と :attr:`Guild.schedule_events` が ``GUILD_UPDATE`` 時に空になる問題を修正しました。" -#: ../../whats_new.rst:241 +#: ../../whats_new.rst:274 msgid "Fix detection of overriden :meth:`app_commands.Group.on_error`" msgstr "オーバーライドされた :meth:`app_commands.Group.on_error` の検出を修正しました。" -#: ../../whats_new.rst:242 +#: ../../whats_new.rst:275 msgid "Fix :meth:`app_commands.CommandTree.on_error` still being called when a bound error handler is set." msgstr "エラーハンドラが設定されている場合にも、 :meth:`app_commands.CommandTree.on_error` が呼び出されているのを修正しました。" -#: ../../whats_new.rst:243 +#: ../../whats_new.rst:276 msgid "Fix thread permissions being set to ``True`` in :meth:`DMChannel.permissions_for` (:issue:`8965`)." msgstr ":meth:`DMChannel.permissions_for` でスレッドの権限が ``True`` に設定されている問題を修正しました。 (:issue:`8965`)" -#: ../../whats_new.rst:244 +#: ../../whats_new.rst:277 msgid "Fix ``on_scheduled_event_delete`` occasionally dispatching with too many parameters (:issue:`9019`)." msgstr "``on_scheduled_event_delete`` に渡されるパラメータが多すぎる場合がある問題を修正しました。 (:issue:`9019`)" -#: ../../whats_new.rst:245 +#: ../../whats_new.rst:278 msgid "|commands| Fix :meth:`Context.from_interaction ` ignoring :attr:`~discord.ext.commands.Context.command_failed`." msgstr "|commands| :meth:`Context.from_interaction ` が :attr:`~discord.ext.commands.Context.command_failed` を無視する問題を修正しました。" -#: ../../whats_new.rst:246 +#: ../../whats_new.rst:279 msgid "|commands| Fix :class:`~discord.ext.commands.Range` to allow 3.10 Union syntax (:issue:`8446`)." msgstr "|commands| :class:`~discord.ext.commands.Range` で 3.10 Union 構文が利用できるようになりました。 (:issue:`8446`)." -#: ../../whats_new.rst:247 +#: ../../whats_new.rst:280 msgid "|commands| Fix ``before_invoke`` not triggering for fallback commands in a hybrid group command (:issue:`8461`, :issue:`8462`)." msgstr "|commands| HybridGroupコマンドでfallbackコマンドがトリガーされない問題を修正しました(:issue:`8461`, :issue:`8462`)。" -#: ../../whats_new.rst:252 +#: ../../whats_new.rst:285 msgid "Change error message for unbound callbacks in :class:`app_commands.ContextMenu` to make it clearer that bound methods are not allowed." msgstr ":class:`app_commands.ContextMenu` でバインドされていないコールバックのエラーメッセージを変更し、バインドされているメソッドは使用できないことを明確にしました。" -#: ../../whats_new.rst:253 +#: ../../whats_new.rst:286 msgid "Normalize type formatting in TypeError exceptions (:issue:`8453`)." msgstr "TypeError 例外で型のフォーマットを標準化しました。 (:issue:`8453`)" -#: ../../whats_new.rst:254 +#: ../../whats_new.rst:287 msgid "Change :meth:`VoiceProtocol.on_voice_state_update` and :meth:`VoiceProtocol.on_voice_server_update` parameters to be positional only (:issue:`8463`)." msgstr ":meth:`VoiceProtocol.on_voice_state_update` と :meth:`VoiceProtocol.on_voice_server_update` パラメータを位置指定専用に変更しました。 (:issue:`8463` )" -#: ../../whats_new.rst:255 +#: ../../whats_new.rst:288 msgid "Add support for PyCharm when using the default coloured logger (:issue:`9015`)." msgstr "デフォルトの色付きロガーに、 PyCharm 対応を追加しました。 (:issue:`9015`)" -#: ../../whats_new.rst:260 +#: ../../whats_new.rst:293 msgid "v2.0.1" msgstr "v2.0.1" -#: ../../whats_new.rst:265 +#: ../../whats_new.rst:298 msgid "Fix ``cchardet`` being installed on Python >=3.10 when using the ``speed`` extras." msgstr "Python 3.10 以降で ``speed`` extrasを使用した場合に ``cchardet`` がインストールされる問題を修正しました。" -#: ../../whats_new.rst:266 +#: ../../whats_new.rst:299 msgid "Fix :class:`ui.View` timeout updating when the :meth:`ui.View.interaction_check` failed." msgstr ":meth:`ui.View.interaction_check` に失敗したときにも :class:`ui.View` のタイムアウトが更新される問題を修正しました。" -#: ../../whats_new.rst:267 +#: ../../whats_new.rst:300 msgid "Fix :meth:`app_commands.CommandTree.on_error` not triggering if :meth:`~app_commands.CommandTree.interaction_check` raises." msgstr ":meth:`~app_commands.CommandTree.interaction_check` が例外を送出したときに :meth:`app_commands.CommandTree.on_error` が実行されない問題を修正しました。" -#: ../../whats_new.rst:268 +#: ../../whats_new.rst:301 msgid "Fix ``__main__`` script to use ``importlib.metadata`` instead of the deprecated ``pkg_resources``." msgstr "非推奨の ``pkg_resources`` の代わりに ``importlib.metadata`` を使用するよう ``__main__`` スクリプトを修正しました。" -#: ../../whats_new.rst:270 +#: ../../whats_new.rst:303 msgid "Fix library callbacks triggering a type checking error if the parameter names were different." msgstr "ライブラリコールバックのパラメータ名が異なる場合に型チェックエラーが検出される問題を修正しました。" -#: ../../whats_new.rst:270 +#: ../../whats_new.rst:303 msgid "This required a change in the :ref:`version_guarantees`" msgstr "これに伴い :ref:`version_guarantees` が改訂されました。" -#: ../../whats_new.rst:272 +#: ../../whats_new.rst:305 msgid "|commands| Fix Python 3.10 union types not working with :class:`commands.Greedy `." msgstr "|commands| Python 3.10 のユニオン型が :class:`commands.Greedy ` で動作しない問題を修正しました。" -#: ../../whats_new.rst:277 +#: ../../whats_new.rst:310 msgid "v2.0.0" msgstr "v2.0.0" -#: ../../whats_new.rst:279 +#: ../../whats_new.rst:312 msgid "The changeset for this version are too big to be listed here, for more information please see :ref:`the migrating page `." msgstr "このバージョンの変更は大きすぎるため、この場所に収まりきりません。詳細については :ref:`移行についてのページ ` を参照してください。" -#: ../../whats_new.rst:285 +#: ../../whats_new.rst:318 msgid "v1.7.3" msgstr "v1.7.3" -#: ../../whats_new.rst:290 +#: ../../whats_new.rst:323 msgid "Fix a crash involving guild uploaded stickers" msgstr "ギルドでアップロードされたスタンプに関するクラッシュを修正しました。" -#: ../../whats_new.rst:291 +#: ../../whats_new.rst:324 msgid "Fix :meth:`DMChannel.permissions_for` not having :attr:`Permissions.read_messages` set." msgstr ":meth:`DMChannel.permissions_for` に :attr:`Permissions.read_messages` が設定されていない問題を修正しました。" -#: ../../whats_new.rst:296 +#: ../../whats_new.rst:329 msgid "v1.7.2" msgstr "v1.7.2" -#: ../../whats_new.rst:301 +#: ../../whats_new.rst:334 msgid "Fix ``fail_if_not_exists`` causing certain message references to not be usable within :meth:`abc.Messageable.send` and :meth:`Message.reply` (:issue:`6726`)" msgstr "``fail_if_not_exists`` により、特定のメッセージ参照が :meth:`abc.Messageable.send` および :meth:`Message.reply` 内で使用できない問題を修正しました。 (:issue:`6726`)" -#: ../../whats_new.rst:302 +#: ../../whats_new.rst:335 msgid "Fix :meth:`Guild.chunk` hanging when the user left the guild. (:issue:`6730`)" msgstr "ギルドからユーザーが脱退した際に :meth:`Guild.chunk` がハングするのを修正しました。 (:issue:`6730`)" -#: ../../whats_new.rst:303 +#: ../../whats_new.rst:336 msgid "Fix loop sleeping after final iteration rather than before (:issue:`6744`)" msgstr "最終反復の前ではなく後にループがスリープするのを修正しました。 (:issue:`6744`)" -#: ../../whats_new.rst:308 +#: ../../whats_new.rst:341 msgid "v1.7.1" msgstr "v1.7.1" -#: ../../whats_new.rst:313 +#: ../../whats_new.rst:346 msgid "|commands| Fix :meth:`Cog.has_error_handler ` not working as intended." msgstr "|commands| :meth:`Cog.has_error_handler ` が正常に動作しない問題を修正しました。" -#: ../../whats_new.rst:318 +#: ../../whats_new.rst:351 msgid "v1.7.0" msgstr "v1.7.0" -#: ../../whats_new.rst:320 +#: ../../whats_new.rst:353 msgid "This version is mainly for improvements and bug fixes. This is more than likely the last major version in the 1.x series. Work after this will be spent on v2.0. As a result, **this is the last version to support Python 3.5**. Likewise, **this is the last version to support user bots**." msgstr "このバージョンは、主にバグ修正と、機能改善が含まれています。 おそらくこのバージョンが、1.xシリーズの最後のメジャーバージョンとなる予定です。これ以降の作業は、主にv2.0に費やされます。 結果として、**このバージョンが、Python 3.5をサポートする最後のバージョンになります**。 同様に、**このバージョンがユーザーボットをサポートする最後のバージョンです**。" -#: ../../whats_new.rst:324 +#: ../../whats_new.rst:357 msgid "Development of v2.0 will have breaking changes and support for newer API features." msgstr "v2.0の開発には、破壊的更新と、新しいAPI機能の変更が含まれるでしょう。" -#: ../../whats_new.rst:329 +#: ../../whats_new.rst:362 msgid "Add support for stage channels via :class:`StageChannel` (:issue:`6602`, :issue:`6608`)" msgstr ":class:`StageChannel` のサポートを追加しました。 (:issue:`6602`, :issue:`6608`)" -#: ../../whats_new.rst:332 +#: ../../whats_new.rst:365 msgid "Add support for :attr:`MessageReference.fail_if_not_exists` (:issue:`6484`)" msgstr ":attr:`MessageReference.fail_if_not_exists` のサポートを追加しました。 (:issue:`6484`)" -#: ../../whats_new.rst:331 +#: ../../whats_new.rst:364 msgid "By default, if the message you're replying to doesn't exist then the API errors out. This attribute tells the Discord API that it's okay for that message to be missing." msgstr "デフォルトでは、もし返信するメッセージが存在しない場合、APIエラーが発生します。この属性は、Discord APIにメッセージが存在していない場合でも、問題がないことを伝えます。" -#: ../../whats_new.rst:334 +#: ../../whats_new.rst:367 msgid "Add support for Discord's new permission serialisation scheme." msgstr "Discordの新しい権限シリアライゼーションスキームのサポートを追加しました。" -#: ../../whats_new.rst:335 +#: ../../whats_new.rst:368 msgid "Add an easier way to move channels using :meth:`abc.GuildChannel.move`" msgstr "簡単にチャンネルの移動をする :meth:`abc.GuildChannel.move` を追加しました。" -#: ../../whats_new.rst:336 +#: ../../whats_new.rst:369 msgid "Add :attr:`Permissions.use_slash_commands`" msgstr "新しい権限 :attr:`Permissions.use_slash_commands` を追加しました。" -#: ../../whats_new.rst:337 +#: ../../whats_new.rst:370 msgid "Add :attr:`Permissions.request_to_speak`" msgstr "新しい権限 :attr:`Permissions.request_to_speak` を追加しました。" -#: ../../whats_new.rst:338 +#: ../../whats_new.rst:371 msgid "Add support for voice regions in voice channels via :attr:`VoiceChannel.rtc_region` (:issue:`6606`)" msgstr ":attr:`VoiceChannel.rtc_region` によるボイスチャンネルの、ボイスリージョンのサポートを追加しました。 (:issue:`6606`)" -#: ../../whats_new.rst:339 +#: ../../whats_new.rst:372 msgid "Add support for :meth:`PartialEmoji.url_as` (:issue:`6341`)" msgstr ":meth:`PartialEmoji.url_as` のサポートを追加しました。 (:issue:`6341`)" -#: ../../whats_new.rst:340 +#: ../../whats_new.rst:373 msgid "Add :attr:`MessageReference.jump_url` (:issue:`6318`)" msgstr ":attr:`MessageReference.jump_url` を追加しました。(:issue:`6318`)" -#: ../../whats_new.rst:341 +#: ../../whats_new.rst:374 msgid "Add :attr:`File.spoiler` (:issue:`6317`)" msgstr ":attr:`File.spoiler` を追加しました。 (:issue:`6317`)" -#: ../../whats_new.rst:342 +#: ../../whats_new.rst:375 msgid "Add support for passing ``roles`` to :meth:`Guild.estimate_pruned_members` (:issue:`6538`)" msgstr "``roles`` を :meth:`Guild.estimate_pruned_members` に渡すことができるようになりました。(:issue:`6538`)" -#: ../../whats_new.rst:343 +#: ../../whats_new.rst:376 msgid "Allow callable class factories to be used in :meth:`abc.Connectable.connect` (:issue:`6478`)" msgstr ":meth:`abc.Connectable.connect` において、クラスを生成する呼び出し可能オブジェクトを使用できるようになりました。( :issue:`6478` )" -#: ../../whats_new.rst:344 +#: ../../whats_new.rst:377 msgid "Add a way to get mutual guilds from the client's cache via :attr:`User.mutual_guilds` (:issue:`2539`, :issue:`6444`)" msgstr "クライアントのキャッシュから共通のギルドを取得する :attr:`User.mutual_guilds` を追加しました。 (:issue:`2539`, :issue:`6444`)" -#: ../../whats_new.rst:345 +#: ../../whats_new.rst:378 msgid ":meth:`PartialMessage.edit` now returns a full :class:`Message` upon success (:issue:`6309`)" msgstr ":meth:`PartialMessage.edit` が成功時に完全な :class:`Message` を返すようになりました。 (:issue:`6309`)" -#: ../../whats_new.rst:346 +#: ../../whats_new.rst:379 msgid "Add :attr:`RawMessageUpdateEvent.guild_id` (:issue:`6489`)" msgstr ":attr:`RawMessageUpdateEvent.guild_id` を追加しました。(:issue:`6489`)" -#: ../../whats_new.rst:347 +#: ../../whats_new.rst:380 msgid ":class:`AuditLogEntry` is now hashable (:issue:`6495`)" msgstr ":class:`AuditLogEntry` がハッシュ可能になりました。 (:issue:`6495`)" -#: ../../whats_new.rst:348 +#: ../../whats_new.rst:381 msgid ":class:`Attachment` is now hashable" msgstr ":class:`Attachment` がハッシュ可能になりました。" -#: ../../whats_new.rst:349 +#: ../../whats_new.rst:382 msgid "Add :attr:`Attachment.content_type` attribute (:issue:`6618`)" msgstr ":attr:`Attachment.content_type` 属性を追加しました。 (:issue:`6618`)" -#: ../../whats_new.rst:350 +#: ../../whats_new.rst:383 msgid "Add support for casting :class:`Attachment` to :class:`str` to get the URL." msgstr "URLを取得するために :class:`Atachment` を :class:`str` へキャストできるようになりました。" -#: ../../whats_new.rst:352 +#: ../../whats_new.rst:385 msgid "Add ``seed`` parameter for :class:`Colour.random` (:issue:`6562`)" msgstr ":class:`Colour.random` に ``seed`` パラメータを追加しました。 (:issue:`6562`)" -#: ../../whats_new.rst:352 +#: ../../whats_new.rst:385 msgid "This only seeds it for one call. If seeding for multiple calls is desirable, use :func:`random.seed`." msgstr "これは1つの呼び出しに対してのみシードします。複数の呼び出しに対するシードが望ましい場合は、 :func:`random.seed` を使用してください。" -#: ../../whats_new.rst:354 +#: ../../whats_new.rst:387 msgid "Add a :func:`utils.remove_markdown` helper function (:issue:`6573`)" msgstr ":func:`utils.remove_markdown` ヘルパー関数を追加しました。 (:issue:`6573`)" -#: ../../whats_new.rst:355 +#: ../../whats_new.rst:388 msgid "Add support for passing scopes to :func:`utils.oauth_url` (:issue:`6568`)" msgstr ":func:`utils.oauth_url` にスコープを渡すことが可能になりました。 (:issue:`6568`)" -#: ../../whats_new.rst:356 +#: ../../whats_new.rst:389 msgid "|commands| Add support for ``rgb`` CSS function as a parameter to :class:`ColourConverter ` (:issue:`6374`)" msgstr "|commands| :class:`ColourConverter ` において、 ``rgb`` CSS関数の文字列を変換できるようになりました。 (:issue:`6374`)" -#: ../../whats_new.rst:357 +#: ../../whats_new.rst:390 msgid "|commands| Add support for converting :class:`StoreChannel` via :class:`StoreChannelConverter ` (:issue:`6603`)" msgstr "|commands| :class:`StoreChannel` を :class:`StoreChannelConverter ` によって変換できるようになりました。 (:issue:`6603`)" -#: ../../whats_new.rst:358 +#: ../../whats_new.rst:391 msgid "|commands| Add support for stripping whitespace after the prefix is encountered using the ``strip_after_prefix`` :class:`~ext.commands.Bot` constructor parameter." msgstr "|commands| :class:`~ext.commands.Bot` の ``strip_after_prefix`` 初期化パラメーターを指定することで、プレフィックスのあとの空白を取り除けるようになりました。" -#: ../../whats_new.rst:359 +#: ../../whats_new.rst:392 msgid "|commands| Add :attr:`Context.invoked_parents ` to get the aliases a command's parent was invoked with (:issue:`1874`, :issue:`6462`)" msgstr "|commands| :attr:`Context.invoked_parents ` で、呼び出されたときのコマンドの親のエイリアスを取得できるようになりました。 (:issue:`1874`, :issue:`6462`)" -#: ../../whats_new.rst:360 +#: ../../whats_new.rst:393 msgid "|commands| Add a converter for :class:`PartialMessage` under :class:`ext.commands.PartialMessageConverter` (:issue:`6308`)" msgstr "|commands| :class:`PartialMessage` 用のコンバーター :class:`ext.commands.PartialMessageConverter` を追加しました。 (:issue:`6308`)" -#: ../../whats_new.rst:361 +#: ../../whats_new.rst:394 msgid "|commands| Add a converter for :class:`Guild` under :class:`ext.commands.GuildConverter` (:issue:`6016`, :issue:`6365`)" msgstr "|commands| :class:`Guild` 用のコンバーター :class:`ext.commands.GuildConverter` を追加しました。 (:issue:`6016`, :issue:`6365`)" -#: ../../whats_new.rst:362 +#: ../../whats_new.rst:395 msgid "|commands| Add :meth:`Command.has_error_handler `" msgstr "|commands| :meth:`Command.has_error_handler ` を追加しました。" -#: ../../whats_new.rst:363 +#: ../../whats_new.rst:396 msgid "This is also adds :meth:`Cog.has_error_handler `" msgstr ":meth:`Cog.has_error_handler ` も追加されました。" -#: ../../whats_new.rst:364 +#: ../../whats_new.rst:397 msgid "|commands| Allow callable types to act as a bucket key for cooldowns (:issue:`6563`)" msgstr "|commands| 呼び出し可能オブジェクトをクールダウンのバケットキーとして使用できるようになりました。 (:issue:`6563`)" -#: ../../whats_new.rst:365 +#: ../../whats_new.rst:398 msgid "|commands| Add ``linesep`` keyword argument to :class:`Paginator ` (:issue:`5975`)" msgstr "|commands| :class:`Paginator ` に ``linesep`` キーワード引数を追加しました。 (:issue:`5975`)" -#: ../../whats_new.rst:366 +#: ../../whats_new.rst:399 msgid "|commands| Allow ``None`` to be passed to :attr:`HelpCommand.verify_checks ` to only verify in a guild context (:issue:`2008`, :issue:`6446`)" msgstr "|commands| :attr:`HelpCommand.verify_checks ` に ``None`` を指定することで、サーバー内でのみチェックを確認するようにできるようにしました。 (:issue:`2008`, :issue:`6446`)" -#: ../../whats_new.rst:367 +#: ../../whats_new.rst:400 msgid "|commands| Allow relative paths when loading extensions via a ``package`` keyword argument (:issue:`2465`, :issue:`6445`)" msgstr "|commands| ``package`` キーワード引数で、エクステンションをロードするときの相対パスを指定できるようになりました。 (:issue:`2465`, :issue:`6445`)" -#: ../../whats_new.rst:372 +#: ../../whats_new.rst:405 msgid "Fix mentions not working if ``mention_author`` is passed in :meth:`abc.Messageable.send` without :attr:`Client.allowed_mentions` set (:issue:`6192`, :issue:`6458`)" msgstr ":attr:`Client.allowed_mentions` が設定されていないときに、 :meth:`abc.Messageable.send` で ``mention_author`` が渡されてもメンションしない問題を修正しました。 (:issue:`6192`, :issue:`6458`)" -#: ../../whats_new.rst:373 +#: ../../whats_new.rst:406 msgid "Fix user created instances of :class:`CustomActivity` triggering an error (:issue:`4049`)" msgstr "ユーザーが作成した :class:`CustomActivity` インスタンスがエラーを引き起こす問題を修正しました。 (:issue:`4049`)" -#: ../../whats_new.rst:374 +#: ../../whats_new.rst:407 msgid "Note that currently, bot users still cannot set a custom activity due to a Discord limitation." msgstr "現在、Discordの制限により、Botはまだカスタムアクティビティを設定できません。" -#: ../../whats_new.rst:375 +#: ../../whats_new.rst:408 msgid "Fix :exc:`ZeroDivisionError` being raised from :attr:`VoiceClient.average_latency` (:issue:`6430`, :issue:`6436`)" msgstr ":attr:`VoiceClient.average_latency` にて :exc:`ZeroDivisionError` が発生する問題を修正しました。 (:issue:`6430`, :issue:`6436`)" -#: ../../whats_new.rst:376 +#: ../../whats_new.rst:409 msgid "Fix :attr:`User.public_flags` not updating upon edit (:issue:`6315`)" msgstr ":attr:`User.public_flags` が編集時に更新されない問題を修正しました。 (:issue:`6315`)" -#: ../../whats_new.rst:377 +#: ../../whats_new.rst:410 msgid "Fix :attr:`Message.call` sometimes causing attribute errors (:issue:`6390`)" msgstr ":attr:`Message.call` が時々AttributeErrorを送出する問題を修正しました。 (:issue:`6390`)" -#: ../../whats_new.rst:378 +#: ../../whats_new.rst:411 msgid "Fix issue resending a file during request retries on newer versions of ``aiohttp`` (:issue:`6531`)" msgstr "新しいバージョンの ``aiohttp`` で、リクエストの再試行中にファイルを再送するときに発生する問題を修正しました。 (:issue:`6531`)" -#: ../../whats_new.rst:379 +#: ../../whats_new.rst:412 msgid "Raise an error when ``user_ids`` is empty in :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` を呼び出す際、 ``user_ids`` に空のリストが指定された際にエラーが発生するようになりました。" -#: ../../whats_new.rst:380 +#: ../../whats_new.rst:413 msgid "Fix ``__str__`` magic method raising when a :class:`Guild` is unavailable." msgstr ":class:`Guild` が利用不可能なときに ``__str__`` メソッドがエラーを出す問題を修正しました。" -#: ../../whats_new.rst:381 +#: ../../whats_new.rst:414 msgid "Fix potential :exc:`AttributeError` when accessing :attr:`VoiceChannel.members` (:issue:`6602`)" msgstr ":attr:`VoiceChannel.members` にアクセスする時に :exc:`AttributeError` が発生する潜在的なバグを修正しました。(:issue:`6602`)" -#: ../../whats_new.rst:382 +#: ../../whats_new.rst:415 msgid ":class:`Embed` constructor parameters now implicitly convert to :class:`str` (:issue:`6574`)" msgstr ":class:`Embed` の初期化時に指定された引数は暗黙的に :class:`str` へ変換されるようになりました。 (:issue:`6574`)" -#: ../../whats_new.rst:383 +#: ../../whats_new.rst:416 msgid "Ensure ``discord`` package is only run if executed as a script (:issue:`6483`)" msgstr "``discord`` パッケージがスクリプトとして実行された場合のみ実行されるようになりました。 (:issue:`6483`)" -#: ../../whats_new.rst:384 +#: ../../whats_new.rst:417 msgid "|commands| Fix irrelevant commands potentially being unloaded during cog unload due to failure." msgstr "|commands| コグのアンロード中、失敗することにより無関係なコマンドがアンロードされる可能性がある問題を修正しました。" -#: ../../whats_new.rst:385 +#: ../../whats_new.rst:418 msgid "|commands| Fix attribute errors when setting a cog to :class:`~.ext.commands.HelpCommand` (:issue:`5154`)" msgstr "|commands| コグを :class:`~.ext.commands.HelpCommand` に設定した際にAttributeErrorが出る問題を修正しました。 (:issue:`5154`)" -#: ../../whats_new.rst:386 +#: ../../whats_new.rst:419 msgid "|commands| Fix :attr:`Context.invoked_with ` being improperly reassigned during a :meth:`~ext.commands.Context.reinvoke` (:issue:`6451`, :issue:`6462`)" msgstr "|commands| :meth:`~ext.commands.Context.reinvoke` 中に :attr:`Context.invoked_with ` が不適切に再割り当てされる問題を修正しました。 (:issue:`6451`, :issue:`6462`)" -#: ../../whats_new.rst:387 +#: ../../whats_new.rst:420 msgid "|commands| Remove duplicates from :meth:`HelpCommand.get_bot_mapping ` (:issue:`6316`)" msgstr "|commands| :meth:`HelpCommand.get_bot_mapping ` で、コマンドが重複する問題を修正しました。 (:issue:`6316`)" -#: ../../whats_new.rst:388 +#: ../../whats_new.rst:421 msgid "|commands| Properly handle positional-only parameters in bot command signatures (:issue:`6431`)" msgstr "|commands| Botのコマンドシグネチャーで、位置限定引数を適切に処理するようになりました。 (:issue:`6431`)" -#: ../../whats_new.rst:389 +#: ../../whats_new.rst:422 msgid "|commands| Group signatures now properly show up in :attr:`Command.signature ` (:issue:`6529`, :issue:`6530`)" msgstr "|commands| グループのシグネチャーが :attr:`Command.signature ` に正しく表示されるようになりました。 (:issue:`6529`, :issue:`6530`)" -#: ../../whats_new.rst:394 +#: ../../whats_new.rst:427 msgid "User endpoints and all userbot related functionality has been deprecated and will be removed in the next major version of the library." msgstr "ユーザー用エンドポイントとユーザーボットの関連機能は非推奨になり、次のライブラリのメジャーバージョンで削除されます。" -#: ../../whats_new.rst:395 +#: ../../whats_new.rst:428 msgid ":class:`Permission` class methods were updated to match the UI of the Discord client (:issue:`6476`)" msgstr ":class:`Permission` のクラスメソッドがDiscordクライアントのUIと一致するように更新されました。 (:issue:`6476`)" -#: ../../whats_new.rst:396 +#: ../../whats_new.rst:429 msgid "``_`` and ``-`` characters are now stripped when making a new cog using the ``discord`` package (:issue:`6313`)" msgstr "``_`` と ``-`` の文字が ``discord`` パッケージを使用して新しいコグを作成するときに取り除かれるようになりました。 (:issue:`6313`)" -#: ../../whats_new.rst:401 +#: ../../whats_new.rst:434 msgid "v1.6.0" msgstr "v1.6.0" -#: ../../whats_new.rst:403 +#: ../../whats_new.rst:436 msgid "This version comes with support for replies and stickers." msgstr "このバージョンでは、返信機能とスタンプ機能がサポートされるようになりました。" -#: ../../whats_new.rst:408 +#: ../../whats_new.rst:441 msgid "An entirely redesigned documentation. This was the cumulation of multiple months of effort." msgstr "完全に再設計されたドキュメント。 これは何ヶ月もの努力の積み重ねで作られました。" -#: ../../whats_new.rst:409 +#: ../../whats_new.rst:442 msgid "There's now a dark theme, feel free to navigate to the cog on the screen to change your setting, though this should be automatic." msgstr "ダークテーマが実装されました。変更するには、画面上の歯車から設定をしてください。これは自動的に行われます。" -#: ../../whats_new.rst:410 +#: ../../whats_new.rst:443 msgid "Add support for :meth:`AppInfo.icon_url_as` and :meth:`AppInfo.cover_image_url_as` (:issue:`5888`)" msgstr ":meth:`AppInfo.icon_url_as` と :meth:`AppInfo.cover_image_url_as` が追加されました。 (:issue:`5888`)" -#: ../../whats_new.rst:411 +#: ../../whats_new.rst:444 msgid "Add :meth:`Colour.random` to get a random colour (:issue:`6067`)" msgstr "ランダムな色が得られる、 :meth:`Colour.random` が追加されました。 (:issue:`6067`)" -#: ../../whats_new.rst:412 +#: ../../whats_new.rst:445 msgid "Add support for stickers via :class:`Sticker` (:issue:`5946`)" msgstr ":class:`Sticker` によってスタンプがサポートされました。 (:issue:`5946`)" -#: ../../whats_new.rst:416 +#: ../../whats_new.rst:449 msgid "Add support for replying via :meth:`Message.reply` (:issue:`6061`)" msgstr ":meth:`Message.reply` で返信ができるようになりました。 (:issue:`6061`)" -#: ../../whats_new.rst:414 +#: ../../whats_new.rst:447 msgid "This also comes with the :attr:`AllowedMentions.replied_user` setting." msgstr "これには :attr:`AllowedMentions.replied_user` の設定も含まれます。" -#: ../../whats_new.rst:415 +#: ../../whats_new.rst:448 msgid ":meth:`abc.Messageable.send` can now accept a :class:`MessageReference`." msgstr ":meth:`abc.Messageable.send` が :class:`MessageReference` を受け付けるようになりました。" -#: ../../whats_new.rst:416 +#: ../../whats_new.rst:449 msgid ":class:`MessageReference` can now be constructed by users." msgstr ":class:`MessageReference` がユーザーによって生成できるようになりました。" -#: ../../whats_new.rst:417 +#: ../../whats_new.rst:450 msgid ":meth:`Message.to_reference` can now convert a message to a :class:`MessageReference`." msgstr ":meth:`Message.to_reference` によってMessageオブジェクトを :class:`MessageReference` に変換できるようになりました。" -#: ../../whats_new.rst:418 +#: ../../whats_new.rst:451 msgid "Add support for getting the replied to resolved message through :attr:`MessageReference.resolved`." msgstr ":attr:`MessageReference.resolved` で解決済みメッセージを得ることができます。" -#: ../../whats_new.rst:424 +#: ../../whats_new.rst:457 msgid "Add support for role tags." msgstr "ロールのタグがサポートされました。" -#: ../../whats_new.rst:420 +#: ../../whats_new.rst:453 msgid ":attr:`Guild.premium_subscriber_role` to get the \"Nitro Booster\" role (if available)." msgstr ":attr:`Guild.premium_subscriber_role` で ニトロブースターロールを取得できます(利用可能な場合)。" -#: ../../whats_new.rst:421 +#: ../../whats_new.rst:454 msgid ":attr:`Guild.self_role` to get the bot's own role (if available)." msgstr ":attr:`Guild.self_role` でサーバー内のBot自身のロールを取得できます(利用可能な場合)。" -#: ../../whats_new.rst:422 +#: ../../whats_new.rst:455 msgid ":attr:`Role.tags` to get the role's tags." msgstr ":attr:`Role.tags` でロールのタグを取得できます。" -#: ../../whats_new.rst:423 +#: ../../whats_new.rst:456 msgid ":meth:`Role.is_premium_subscriber` to check if a role is the \"Nitro Booster\" role." msgstr ":meth:`Role.is_premium_subscriber` でロールがニトロブースターロールであるかを確認できます。" -#: ../../whats_new.rst:424 +#: ../../whats_new.rst:457 msgid ":meth:`Role.is_bot_managed` to check if a role is a bot role (i.e. the automatically created role for bots)." msgstr ":meth:`Role.is_bot_managed` でロールがボットロール(自動的に作られたBot用ロール)であるかを確認できます。" -#: ../../whats_new.rst:425 +#: ../../whats_new.rst:458 msgid ":meth:`Role.is_integration` to check if a role is role created by an integration." msgstr ":meth:`Role.is_integration` でインテグレーションによって作成されたロールかどうか確認できます。" -#: ../../whats_new.rst:426 +#: ../../whats_new.rst:459 msgid "Add :meth:`Client.is_ws_ratelimited` to check if the websocket is rate limited." msgstr ":meth:`Client.is_ws_ratelimited` でWebSocketのレート制限がされているかどうか確認できるようになりました。" -#: ../../whats_new.rst:427 +#: ../../whats_new.rst:460 msgid ":meth:`ShardInfo.is_ws_ratelimited` is the equivalent for checking a specific shard." msgstr ":meth:`ShardInfo.is_ws_ratelimited` は特定のシャードのWebSocketレート制限をチェックします。" -#: ../../whats_new.rst:428 +#: ../../whats_new.rst:461 msgid "Add support for chunking an :class:`AsyncIterator` through :meth:`AsyncIterator.chunk` (:issue:`6100`, :issue:`6082`)" msgstr ":class:`AsyncIterator` を :meth:`AsyncIterator.chunk` を通してチャンク化できるようになりました。 (:issue:`6100`, :issue:`6082`)" -#: ../../whats_new.rst:429 +#: ../../whats_new.rst:462 msgid "Add :attr:`PartialEmoji.created_at` (:issue:`6128`)" msgstr ":attr:`PartialEmoji.created_at` を追加しました。 (:issue:`6128`)" -#: ../../whats_new.rst:430 +#: ../../whats_new.rst:463 msgid "Add support for editing and deleting webhook sent messages (:issue:`6058`)" msgstr "Webhookで送信したメッセージの編集と削除をサポートしました。 (:issue:`6058`)" -#: ../../whats_new.rst:431 +#: ../../whats_new.rst:464 msgid "This adds :class:`WebhookMessage` as well to power this behaviour." msgstr "この機能のために :class:`WebhookMessage` が追加されました。" -#: ../../whats_new.rst:432 +#: ../../whats_new.rst:465 msgid "Add :class:`PartialMessage` to allow working with a message via channel objects and just a message_id (:issue:`5905`)" msgstr "チャンネルオブジェクトとメッセージIDのみでメッセージを操作できるようにするために、 :class:`PartialMessage` を追加しました。 (:issue:`5905`)" -#: ../../whats_new.rst:433 +#: ../../whats_new.rst:466 msgid "This is useful if you don't want to incur an extra API call to fetch the message." msgstr "これはメッセージを取得するために追加のAPI呼び出しをしたくないときに便利です。" -#: ../../whats_new.rst:434 +#: ../../whats_new.rst:467 msgid "Add :meth:`Emoji.url_as` (:issue:`6162`)" msgstr ":meth:`Emoji.url_as` を追加しました。 (:issue:`6162`)" -#: ../../whats_new.rst:435 +#: ../../whats_new.rst:468 msgid "Add support for :attr:`Member.pending` for the membership gating feature." msgstr "メンバーシップゲート機能用に :attr:`Member.pending` のサポートを追加しました。" -#: ../../whats_new.rst:436 +#: ../../whats_new.rst:469 msgid "Allow ``colour`` parameter to take ``int`` in :meth:`Guild.create_role` (:issue:`6195`)" msgstr ":meth:`Guild.create_role` で ``colour`` パラメータに ``int`` 型を渡すことができるようになりました。 (:issue:`6195`)" -#: ../../whats_new.rst:437 +#: ../../whats_new.rst:470 msgid "Add support for ``presences`` in :meth:`Guild.query_members` (:issue:`2354`)" msgstr ":meth:`Guild.query_members` で、 ``presences`` 引数が使えるようになりました。 (:issue:`2354`)" -#: ../../whats_new.rst:438 +#: ../../whats_new.rst:471 msgid "|commands| Add support for ``description`` keyword argument in :class:`commands.Cog ` (:issue:`6028`)" msgstr "|commands| :class:`commands.Cog ` において、 ``description`` キーワード引数が使えるようになりました。 (:issue:`6028`)" -#: ../../whats_new.rst:439 +#: ../../whats_new.rst:472 msgid "|tasks| Add support for calling the wrapped coroutine as a function via ``__call__``." msgstr "|tasks| ``__call__`` を使うことによってラップされたコルーチン関数を呼び出せるようになりました。" -#: ../../whats_new.rst:445 +#: ../../whats_new.rst:478 msgid "Raise :exc:`DiscordServerError` when reaching 503s repeatedly (:issue:`6044`)" msgstr "HTTPリクエスト時にステータス503が繰り返し返されたとき、 :exc:`DiscordServerError` が出るようになりました。 (:issue:`6044`)" -#: ../../whats_new.rst:446 +#: ../../whats_new.rst:479 msgid "Fix :exc:`AttributeError` when :meth:`Client.fetch_template` is called (:issue:`5986`)" msgstr ":meth:`Client.fetch_template` が呼び出されたとき :exc:`AttributeError` が出る問題を修正しました。 (:issue:`5986`)" -#: ../../whats_new.rst:447 +#: ../../whats_new.rst:480 msgid "Fix errors when playing audio and moving to another channel (:issue:`5953`)" msgstr "音声を再生するときと別のボイスチャンネルへ移動するときに発生するエラーを修正しました。 (:issue:`5953`)" -#: ../../whats_new.rst:448 +#: ../../whats_new.rst:481 msgid "Fix :exc:`AttributeError` when voice channels disconnect too fast (:issue:`6039`)" msgstr "ボイスチャンネルから切断するのが速すぎるときに発生する :exc:`AttributeError` を修正しました。 (:issue:`6039`)" -#: ../../whats_new.rst:449 +#: ../../whats_new.rst:482 msgid "Fix stale :class:`User` references when the members intent is off." msgstr "memberインテントがオフの場合に :class:`User` の参照が古くなってしまう問題を修正しました。" -#: ../../whats_new.rst:450 +#: ../../whats_new.rst:483 msgid "Fix :func:`on_user_update` not dispatching in certain cases when a member is not cached but the user somehow is." msgstr "memberがキャッシュされておらず、userが何らかの形でキャッシュされている場合に :func:`on_user_update` が発火されない問題を修正しました。" -#: ../../whats_new.rst:451 +#: ../../whats_new.rst:484 msgid "Fix :attr:`Message.author` being overwritten in certain cases during message update." msgstr "メッセージが更新されているとき、特定のケースで :attr:`Message.author` が上書きされてしまう問題を修正しました。" -#: ../../whats_new.rst:452 +#: ../../whats_new.rst:485 msgid "This would previously make it so :attr:`Message.author` is a :class:`User`." msgstr "これにより、 :attr:`Message.author` が :class:`User` になるようになりました。" -#: ../../whats_new.rst:453 +#: ../../whats_new.rst:486 msgid "Fix :exc:`UnboundLocalError` for editing ``public_updates_channel`` in :meth:`Guild.edit` (:issue:`6093`)" msgstr ":meth:`Guild.edit` で ``public_updates_channel`` を変更する際に :exc:`UnboundLocalError` が発生する問題を修正しました。 (:issue:`6093`)" -#: ../../whats_new.rst:454 +#: ../../whats_new.rst:487 msgid "Fix uninitialised :attr:`CustomActivity.created_at` (:issue:`6095`)" msgstr ":attr:`CustomActivity.created_at` が初期化されない問題を修正しました。 (:issue:`6095`)" -#: ../../whats_new.rst:455 +#: ../../whats_new.rst:488 msgid "|commands| Errors during cog unload no longer stops module cleanup (:issue:`6113`)" msgstr "|commands| コグのアンロード中に起きたエラーがモジュールのcleanupを止めないようになりました。 (:issue:`6113`)" -#: ../../whats_new.rst:456 +#: ../../whats_new.rst:489 msgid "|commands| Properly cleanup lingering commands when a conflicting alias is found when adding commands (:issue:`6217`)" msgstr "|commands| コマンドを追加する際、エイリアスが競合したときに残ってしまうエイリアスを適切にクリーンアップするようになりました。 (:issue:`6217`)" -#: ../../whats_new.rst:461 +#: ../../whats_new.rst:494 msgid "``ffmpeg`` spawned processes no longer open a window in Windows (:issue:`6038`)" msgstr "Windowsにおいて呼び出された ``ffmpeg`` がウィンドウを開かないようになりました。 (:issue:`6038`)" -#: ../../whats_new.rst:462 +#: ../../whats_new.rst:495 msgid "Update dependencies to allow the library to work on Python 3.9+ without requiring build tools. (:issue:`5984`, :issue:`5970`)" msgstr "ライブラリがビルドツールなしでPython3.9以上で動作するよう、依存関係を変更しました。 (:issue:`5984`, :issue:`5970`)" -#: ../../whats_new.rst:463 +#: ../../whats_new.rst:496 msgid "Fix docstring issue leading to a SyntaxError in 3.9 (:issue:`6153`)" msgstr "Python3.9においてSyntaxErrorになるdocstringの問題を修正しました。 (:issue:`6153`)" -#: ../../whats_new.rst:464 +#: ../../whats_new.rst:497 msgid "Update Windows opus binaries from 1.2.1 to 1.3.1 (:issue:`6161`)" msgstr "Windows用のopusバイナリをバージョン1.2.1から1.3.1に更新しました。 (:issue:`6161`)" -#: ../../whats_new.rst:465 +#: ../../whats_new.rst:498 msgid "Allow :meth:`Guild.create_role` to accept :class:`int` as the ``colour`` parameter (:issue:`6195`)" msgstr ":meth:`Guild.create_role` の ``colour`` 引数で :class:`int` 型が使えるようになりました。\n" "(:issue:`6195`)" -#: ../../whats_new.rst:466 +#: ../../whats_new.rst:499 msgid "|commands| :class:`MessageConverter ` regex got updated to support ``www.`` prefixes (:issue:`6002`)" msgstr "|commands| :class:`MessageConverter ` のregexが ``www.`` プレフィックスをサポートするように更新されました。 (:issue:`6002`)" -#: ../../whats_new.rst:467 +#: ../../whats_new.rst:500 msgid "|commands| :class:`UserConverter ` now fetches the API if an ID is passed and the user is not cached." msgstr "|commands| :class:`UserConverter ` は、IDが渡され、そのユーザーがキャッシュされていない場合にAPIからデータを取得するようになりました。" -#: ../../whats_new.rst:468 +#: ../../whats_new.rst:501 msgid "|commands| :func:`max_concurrency ` is now called before cooldowns (:issue:`6172`)" msgstr "|commands| :func:`max_concurrency ` がクールダウンの前に呼び出されるようになりました。 (:issue:`6172`)" -#: ../../whats_new.rst:473 +#: ../../whats_new.rst:506 msgid "v1.5.1" msgstr "v1.5.1" -#: ../../whats_new.rst:478 +#: ../../whats_new.rst:511 msgid "Fix :func:`utils.escape_markdown` not escaping quotes properly (:issue:`5897`)" msgstr ":func:`utils.escape_markdown` が引用符を正しくエスケープしない問題を修正しました。 (:issue:`5897`)" -#: ../../whats_new.rst:479 +#: ../../whats_new.rst:512 msgid "Fix :class:`Message` not being hashable (:issue:`5901`, :issue:`5866`)" msgstr ":class:`Message` がハッシュ可能でない問題を修正しました。 (:issue:`5901`, :issue:`5866`)" -#: ../../whats_new.rst:480 +#: ../../whats_new.rst:513 msgid "Fix moving channels to the end of the channel list (:issue:`5923`)" msgstr "チャンネルをチャンネルリストの最後まで移動する際の問題を修正しました。 (:issue:`5923`)" -#: ../../whats_new.rst:481 +#: ../../whats_new.rst:514 msgid "Fix seemingly strange behaviour in ``__eq__`` for :class:`PermissionOverwrite` (:issue:`5929`)" msgstr ":class:`PermissionOverwrite` における ``__eq__`` のおかしい挙動を修正しました。 (:issue:`5929`)" -#: ../../whats_new.rst:482 +#: ../../whats_new.rst:515 msgid "Fix aliases showing up in ``__iter__`` for :class:`Intents` (:issue:`5945`)" msgstr ":class:`Intents` の ``__iter__`` におけるエイリアスの表示の問題を修正しました。 (:issue:`5945`)" -#: ../../whats_new.rst:483 +#: ../../whats_new.rst:516 msgid "Fix the bot disconnecting from voice when moving them to another channel (:issue:`5904`)" msgstr "別のボイスチャンネルに移動する時にBotがボイスチャンネルから切断されてしまう問題を修正しました。 (:issue:`5945`)" -#: ../../whats_new.rst:484 +#: ../../whats_new.rst:517 msgid "Fix attribute errors when chunking times out sometimes during delayed on_ready dispatching." msgstr "遅延on_readyディスパッチ中にチャンキングがタイムアウトする場合の属性エラーを修正しました。" -#: ../../whats_new.rst:485 +#: ../../whats_new.rst:518 msgid "Ensure that the bot's own member is not evicted from the cache (:issue:`5949`)" msgstr "Bot自身のmemberオブジェクトがキャッシュから削除されないことが保証されるようになりました。 (:issue:`5949`)" -#: ../../whats_new.rst:490 +#: ../../whats_new.rst:523 msgid "Members are now loaded during ``GUILD_MEMBER_UPDATE`` events if :attr:`MemberCacheFlags.joined` is set. (:issue:`5930`)" msgstr ":attr:`MemberCacheFlags.joined` が設定されている場合、memberが ``GUILD_MEMBER_UPDATE`` イベントでロードされるようになりました。 (:issue:`5930`)" -#: ../../whats_new.rst:491 +#: ../../whats_new.rst:524 msgid "|commands| :class:`MemberConverter ` now properly lazily fetches members if not available from cache." msgstr "|commands| :class:`MemberConverter ` は、memberがキャッシュから利用できない場合に遅延ロードでmemberを取得するようになりました。" -#: ../../whats_new.rst:492 +#: ../../whats_new.rst:525 msgid "This is the same as having ``discord.Member`` as the type-hint." msgstr "これは ``discord.Member`` を型ヒントとして使うのと同じです。" -#: ../../whats_new.rst:493 +#: ../../whats_new.rst:526 msgid ":meth:`Guild.chunk` now allows concurrent calls without spamming the gateway with requests." msgstr ":meth:`Guild.chunk` によって、Gatewayに負荷をかけずに同時呼び出しができるようになりました。" -#: ../../whats_new.rst:498 +#: ../../whats_new.rst:531 msgid "v1.5.0" msgstr "v1.5.0" -#: ../../whats_new.rst:500 +#: ../../whats_new.rst:533 msgid "This version came with forced breaking changes that Discord is requiring all bots to go through on October 7th. It is highly recommended to read the documentation on intents, :ref:`intents_primer`." msgstr "このバージョンでは、Discordが10月7日に行う、すべてのBotに要求している強制的な破壊的変更が含まれています。Intentsに関するドキュメント :ref:`intents_primer` を読むことを強くおすすめします。" -#: ../../whats_new.rst:503 +#: ../../whats_new.rst:536 msgid "API Changes" msgstr "APIの変更" -#: ../../whats_new.rst:505 +#: ../../whats_new.rst:538 msgid "Members and presences will no longer be retrieved due to an API change. See :ref:`privileged_intents` for more info." msgstr "APIの変更により、memberとpresenceの情報は取得されなくなります。 詳細は :ref:`privileged_intents` を参照してください。" -#: ../../whats_new.rst:506 +#: ../../whats_new.rst:539 msgid "As a consequence, fetching offline members is disabled if the members intent is not enabled." msgstr "結果として、 memberインテントが有効でない場合、オフラインメンバーの取得が無効になります。" -#: ../../whats_new.rst:511 +#: ../../whats_new.rst:544 msgid "Support for gateway intents, passed via ``intents`` in :class:`Client` using :class:`Intents`." msgstr ":class:`Client` において、 ``intents`` 引数に :class:`Intents` を渡すことでゲートウェイインテントがサポートされるようになりました。" -#: ../../whats_new.rst:512 +#: ../../whats_new.rst:545 msgid "Add :attr:`VoiceRegion.south_korea` (:issue:`5233`)" msgstr ":attr:`VoiceRegion.south_korea` が追加されました。 (:issue:`5233`)" -#: ../../whats_new.rst:513 +#: ../../whats_new.rst:546 msgid "Add support for ``__eq__`` for :class:`Message` (:issue:`5789`)" msgstr ":class:`Message` において、 ``__eq__`` がサポートされました。 (:issue:`5789`)" -#: ../../whats_new.rst:514 +#: ../../whats_new.rst:547 msgid "Add :meth:`Colour.dark_theme` factory method (:issue:`1584`)" msgstr ":meth:`Colour.dark_theme` クラスメソッドが追加されました。 (:issue:`1584`)" -#: ../../whats_new.rst:515 +#: ../../whats_new.rst:548 msgid "Add :meth:`AllowedMentions.none` and :meth:`AllowedMentions.all` (:issue:`5785`)" msgstr ":meth:`AllowedMentions.none` と :meth:`AllowedMentions.all` が追加されました。 (:issue:`5785`)" -#: ../../whats_new.rst:516 +#: ../../whats_new.rst:549 msgid "Add more concrete exceptions for 500 class errors under :class:`DiscordServerError` (:issue:`5797`)" msgstr ":class:`DiscordServerError` のサブクラスとして、 ステータス500エラーの具体的な例外を追加しました。 (:issue:`5797`)" -#: ../../whats_new.rst:517 +#: ../../whats_new.rst:550 msgid "Implement :class:`VoiceProtocol` to better intersect the voice flow." msgstr "音声フローをより良く交差させるため、 :class:`VoiceProtocol` を実装しました。" -#: ../../whats_new.rst:518 +#: ../../whats_new.rst:551 msgid "Add :meth:`Guild.chunk` to fully chunk a guild." msgstr "ギルドをチャンク化して取得する :meth:`Guild.chunk` を追加しました。" -#: ../../whats_new.rst:519 +#: ../../whats_new.rst:552 msgid "Add :class:`MemberCacheFlags` to better control member cache. See :ref:`intents_member_cache` for more info." msgstr "メンバーキャッシュをより適切に制御するために :class:`MemberCacheFlags` を追加しました。詳細は :ref:`intents_member_cache` を参照してください。" -#: ../../whats_new.rst:521 +#: ../../whats_new.rst:554 msgid "Add support for :attr:`ActivityType.competing` (:issue:`5823`)" msgstr ":attr:`ActivityType.competing` のサポートを追加しました。 (:issue:`5823`)" -#: ../../whats_new.rst:521 +#: ../../whats_new.rst:554 msgid "This seems currently unused API wise." msgstr "これはAPIとしては現在未使用のようです。" -#: ../../whats_new.rst:523 +#: ../../whats_new.rst:556 msgid "Add support for message references, :attr:`Message.reference` (:issue:`5754`, :issue:`5832`)" msgstr "メッセージ参照のサポート、 :attr:`Message.reference` を追加しました。 (:issue:`5754`, :issue:`5832`)" -#: ../../whats_new.rst:524 +#: ../../whats_new.rst:557 msgid "Add alias for :class:`ColourConverter` under ``ColorConverter`` (:issue:`5773`)" msgstr ":class:`ColourConverter` のエイリアス ``ColorConverter`` を追加しました。 (:issue:`5773`)" -#: ../../whats_new.rst:525 +#: ../../whats_new.rst:558 msgid "Add alias for :attr:`PublicUserFlags.verified_bot_developer` under :attr:`PublicUserFlags.early_verified_bot_developer` (:issue:`5849`)" msgstr ":attr:`PublicUserFlags.verified_bot_developer` のエイリアスを :attr:`PublicUserFlags.early_verified_bot_developer` の下に追加しました。(:issue:`5849`)" -#: ../../whats_new.rst:526 +#: ../../whats_new.rst:559 msgid "|commands| Add support for ``require_var_positional`` for :class:`Command` (:issue:`5793`)" msgstr "|commands| :class:`Command` に ``require_var_positional`` のサポートを追加しました。 (:issue:`5793`)" -#: ../../whats_new.rst:531 -#: ../../whats_new.rst:565 +#: ../../whats_new.rst:564 +#: ../../whats_new.rst:598 msgid "Fix issue with :meth:`Guild.by_category` not showing certain channels." msgstr ":meth:`Guild.by_category` がいくつかのチャンネルを表示しない問題を修正しました。" -#: ../../whats_new.rst:532 -#: ../../whats_new.rst:566 +#: ../../whats_new.rst:565 +#: ../../whats_new.rst:599 msgid "Fix :attr:`abc.GuildChannel.permissions_synced` always being ``False`` (:issue:`5772`)" msgstr ":attr:`abc.GuildChannel.permissions_synced` が常に ``False`` になる問題を修正しました。 (:issue:`5772`)" -#: ../../whats_new.rst:533 -#: ../../whats_new.rst:567 +#: ../../whats_new.rst:566 +#: ../../whats_new.rst:600 msgid "Fix handling of cloudflare bans on webhook related requests (:issue:`5221`)" msgstr "Webhook関連のリクエストでcloudflareにBANされた際の処理に発生するバグを修正しました。(:issue:`5221`)" -#: ../../whats_new.rst:534 -#: ../../whats_new.rst:568 +#: ../../whats_new.rst:567 +#: ../../whats_new.rst:601 msgid "Fix cases where a keep-alive thread would ack despite already dying (:issue:`5800`)" msgstr "キープライブスレッドが既に死んでいるにも関わらずackをするのを修正しました。(:issue:`5800`)" -#: ../../whats_new.rst:535 -#: ../../whats_new.rst:569 +#: ../../whats_new.rst:568 +#: ../../whats_new.rst:602 msgid "Fix cases where a :class:`Member` reference would be stale when cache is disabled in message events (:issue:`5819`)" msgstr "メッセージイベントでキャッシュが無効になった際に、 :class:`Member` の参照が古くなる問題を修正しました。 (:issue:`5819`)" -#: ../../whats_new.rst:536 -#: ../../whats_new.rst:570 +#: ../../whats_new.rst:569 +#: ../../whats_new.rst:603 msgid "Fix ``allowed_mentions`` not being sent when sending a single file (:issue:`5835`)" msgstr "単一のファイルを送信したときに ``allowed_mentions`` が送信されない問題を修正しました。 (:issue:`5835`)" -#: ../../whats_new.rst:537 -#: ../../whats_new.rst:571 +#: ../../whats_new.rst:570 +#: ../../whats_new.rst:604 msgid "Fix ``overwrites`` being ignored in :meth:`abc.GuildChannel.edit` if ``{}`` is passed (:issue:`5756`, :issue:`5757`)" msgstr "``{}`` が渡された場合、 :meth:`abc.GuildChannel.edit` で ``overwrites`` が無視されるのを修正しました。(:issue:`5756`, :issue:`5757`)" -#: ../../whats_new.rst:538 -#: ../../whats_new.rst:572 +#: ../../whats_new.rst:571 +#: ../../whats_new.rst:605 msgid "|commands| Fix exceptions being raised improperly in command invoke hooks (:issue:`5799`)" msgstr "|commands| コマンド呼び出しフックでの例外が正しく送出されない問題を修正しました。 (:issue:`5799`)" -#: ../../whats_new.rst:539 -#: ../../whats_new.rst:573 +#: ../../whats_new.rst:572 +#: ../../whats_new.rst:606 msgid "|commands| Fix commands not being properly ejected during errors in a cog injection (:issue:`5804`)" msgstr "|commands| コグを追加するときにエラーが発生した場合にコマンドが正しく除去されない問題を修正しました。 (:issue:`5804`)" -#: ../../whats_new.rst:540 -#: ../../whats_new.rst:574 +#: ../../whats_new.rst:573 +#: ../../whats_new.rst:607 msgid "|commands| Fix cooldown timing ignoring edited timestamps." msgstr "|commands| クールダウンのタイミングが編集のタイムスタンプを無視していたのを修正しました。" -#: ../../whats_new.rst:541 -#: ../../whats_new.rst:575 +#: ../../whats_new.rst:574 +#: ../../whats_new.rst:608 msgid "|tasks| Fix tasks extending the next iteration on handled exceptions (:issue:`5762`, :issue:`5763`)" msgstr "|tasks| 例外処理後のイテレーションでの問題を修正しました。 (:issue:`5762`, :issue:`5763`)" -#: ../../whats_new.rst:546 +#: ../../whats_new.rst:579 msgid "Webhook requests are now logged (:issue:`5798`)" msgstr "Webhookリクエストをログに記録するように変更しました。 (:issue:`5798`)" -#: ../../whats_new.rst:547 #: ../../whats_new.rst:580 +#: ../../whats_new.rst:613 msgid "Remove caching layer from :attr:`AutoShardedClient.shards`. This was causing issues if queried before launching shards." msgstr ":attr:`AutoShardedClient.shards` からキャッシュレイヤーを削除しました。これは、シャードを起動する前にクエリを実行すると問題が発生するためです。" -#: ../../whats_new.rst:548 +#: ../../whats_new.rst:581 msgid "Gateway rate limits are now handled." msgstr "ゲートウェイレート制限の処理が行われるようになりました。" -#: ../../whats_new.rst:549 +#: ../../whats_new.rst:582 msgid "Warnings logged due to missed caches are now changed to DEBUG log level." msgstr "ミスキャッシュによる警告レベルのログがDEBUGレベルのログに変更されました。" -#: ../../whats_new.rst:550 +#: ../../whats_new.rst:583 msgid "Some strings are now explicitly interned to reduce memory usage." msgstr "一部の文字列は、メモリ使用量を削減するために明示的にインターンされるようになりました。" -#: ../../whats_new.rst:551 +#: ../../whats_new.rst:584 msgid "Usage of namedtuples has been reduced to avoid potential breaking changes in the future (:issue:`5834`)" msgstr "将来的に壊れる可能性のある変更を避けるために、namedtuplesの使用が削減されました。(:issue:`5834`)" -#: ../../whats_new.rst:552 +#: ../../whats_new.rst:585 msgid "|commands| All :class:`BadArgument` exceptions from the built-in converters now raise concrete exceptions to better tell them apart (:issue:`5748`)" msgstr "|commands| ビルトインコンバータから送出されていた全ての :class:`BadArgument` 例外は、判別しやすいよう具体的な例外を発生させるようになりました。 (:issue:`5748`)" -#: ../../whats_new.rst:553 -#: ../../whats_new.rst:581 +#: ../../whats_new.rst:586 +#: ../../whats_new.rst:614 msgid "|tasks| Lazily fetch the event loop to prevent surprises when changing event loop policy (:issue:`5808`)" msgstr "|tasks| Lazily fetch event loop to prevent surprises when changing event loop policy (:issue:`5808`)" -#: ../../whats_new.rst:558 +#: ../../whats_new.rst:591 msgid "v1.4.2" msgstr "v1.4.2" -#: ../../whats_new.rst:560 +#: ../../whats_new.rst:593 msgid "This is a maintenance release with backports from :ref:`vp1p5p0`." msgstr "これは :ref:`vp1p5p0` からのバックポートによるメンテナンスリリースです。" -#: ../../whats_new.rst:586 +#: ../../whats_new.rst:619 msgid "v1.4.1" msgstr "v1.4.1" -#: ../../whats_new.rst:591 +#: ../../whats_new.rst:624 msgid "Properly terminate the connection when :meth:`Client.close` is called (:issue:`5207`)" msgstr ":meth:`Client.close` が呼び出されたときに正常に接続を終了するようにしました。 (:issue:`5207`)" -#: ../../whats_new.rst:592 +#: ../../whats_new.rst:625 msgid "Fix error being raised when clearing embed author or image when it was already cleared (:issue:`5210`, :issue:`5212`)" msgstr "埋め込みの作者や画像がすでにクリアされているときにクリアしようとするとエラーが発生するのを修正しました。 (:issue:`5210`, :issue:`5212`)" -#: ../../whats_new.rst:593 +#: ../../whats_new.rst:626 msgid "Fix ``__path__`` to allow editable extensions (:issue:`5213`)" msgstr "編集可能なエクステンションを利用できるように ``__path__`` を修正しました。 (:issue:`5213`)" -#: ../../whats_new.rst:598 +#: ../../whats_new.rst:631 msgid "v1.4.0" msgstr "v1.4.0" -#: ../../whats_new.rst:600 +#: ../../whats_new.rst:633 msgid "Another version with a long development time. Features like Intents are slated to be released in a v1.5 release. Thank you for your patience!" msgstr "長い開発時間を持つ別のバージョンです。Intentsのような機能はv1.5リリースでリリースされる予定です。ご理解いただきありがとうございます!" -#: ../../whats_new.rst:607 +#: ../../whats_new.rst:640 msgid "Add support for :class:`AllowedMentions` to have more control over what gets mentioned." msgstr "メンションの動作を制御する :class:`AllowedMentions` を追加しました。" -#: ../../whats_new.rst:606 +#: ../../whats_new.rst:639 msgid "This can be set globally through :attr:`Client.allowed_mentions`" msgstr "これは :attr:`Client.allowed_mentions` から設定することができます。" -#: ../../whats_new.rst:607 +#: ../../whats_new.rst:640 msgid "This can also be set on a per message basis via :meth:`abc.Messageable.send`" msgstr ":meth:`abc.Messageable.send` を介してメッセージごとに設定することもできます。" -#: ../../whats_new.rst:615 +#: ../../whats_new.rst:648 msgid ":class:`AutoShardedClient` has been completely redesigned from the ground up to better suit multi-process clusters (:issue:`2654`)" msgstr ":class:`AutoShardedClient` は、マルチプロセスクラスタに適した設計に完全に変更されました。(:issue:`2654`)" -#: ../../whats_new.rst:610 +#: ../../whats_new.rst:643 msgid "Add :class:`ShardInfo` which allows fetching specific information about a shard." msgstr "シャードに関する情報を取得するために :class:`ShardInfo` を追加しました。" -#: ../../whats_new.rst:611 +#: ../../whats_new.rst:644 msgid "The :class:`ShardInfo` allows for reconnecting and disconnecting of a specific shard as well." msgstr ":class:`ShardInfo` では、特定のシャードの再接続と切断も可能です。" -#: ../../whats_new.rst:612 +#: ../../whats_new.rst:645 msgid "Add :meth:`AutoShardedClient.get_shard` and :attr:`AutoShardedClient.shards` to get information about shards." msgstr "シャードに関する情報を取得するための :meth:`AutoShardedClient.get_shard` と :attr:`AutoShardedClient.shards` を追加しました。" -#: ../../whats_new.rst:613 +#: ../../whats_new.rst:646 msgid "Rework the entire connection flow to better facilitate the ``IDENTIFY`` rate limits." msgstr "接続フロー全体をリワークして、``IDENTIFY`` レート制限の対応を改善しました。" -#: ../../whats_new.rst:614 +#: ../../whats_new.rst:647 msgid "Add a hook :meth:`Client.before_identify_hook` to have better control over what happens before an ``IDENTIFY`` is done." msgstr "``IDENTIFY`` が完了する前に何を行うべきかをよりよく制御できる :meth:`Client.before_identify_hook` を追加しました。" -#: ../../whats_new.rst:615 +#: ../../whats_new.rst:648 msgid "Add more shard related events such as :func:`on_shard_connect`, :func:`on_shard_disconnect` and :func:`on_shard_resumed`." msgstr ":func:`on_shard_connect` 、 :func:`on_shard_disconnect` 、 :func:`on_shard_resumed` などのシャード関連イベントを追加しました。" -#: ../../whats_new.rst:621 +#: ../../whats_new.rst:654 msgid "Add support for guild templates (:issue:`2652`)" msgstr "サーバーテンプレートのサポートを追加しました。 (:issue:`2652`)" -#: ../../whats_new.rst:618 +#: ../../whats_new.rst:651 msgid "This adds :class:`Template` to read a template's information." msgstr "テンプレートの情報を読むために :class:`Template` を追加しました。" -#: ../../whats_new.rst:619 +#: ../../whats_new.rst:652 msgid ":meth:`Client.fetch_template` can be used to fetch a template's information from the API." msgstr "テンプレートの情報を API から取得するには :meth:`Client.fetch_template` が使用できます。" -#: ../../whats_new.rst:620 +#: ../../whats_new.rst:653 msgid ":meth:`Client.create_guild` can now take an optional template to base the creation from." msgstr ":meth:`Client.create_guild` は任意で作成元のテンプレートを取ることができます。" -#: ../../whats_new.rst:621 +#: ../../whats_new.rst:654 msgid "Note that fetching a guild's template is currently restricted for bot accounts." msgstr "Botアカウントでは、ギルドのテンプレートの取得は現在制限されていることに注意してください。" -#: ../../whats_new.rst:631 +#: ../../whats_new.rst:664 msgid "Add support for guild integrations (:issue:`2051`, :issue:`1083`)" msgstr "ギルドインテグレーションのサポートを追加しました。 (:issue:`2051`, :issue:`1083`)" -#: ../../whats_new.rst:624 +#: ../../whats_new.rst:657 msgid ":class:`Integration` is used to read integration information." msgstr ":class:`Integration` はインテグレーション情報の読み取りに使用されます。" -#: ../../whats_new.rst:625 +#: ../../whats_new.rst:658 msgid ":class:`IntegrationAccount` is used to read integration account information." msgstr ":class:`IntegrationAccount` はインテグレーションアカウント情報の読み取りに使用されます。" -#: ../../whats_new.rst:626 +#: ../../whats_new.rst:659 msgid ":meth:`Guild.integrations` will fetch all integrations in a guild." msgstr ":meth:`Guild.integrations` はギルド内の全てのインテグレーションを取得します。" -#: ../../whats_new.rst:627 +#: ../../whats_new.rst:660 msgid ":meth:`Guild.create_integration` will create an integration." msgstr ":meth:`Guild.create_integration` はインテグレーションを作成します。" -#: ../../whats_new.rst:628 +#: ../../whats_new.rst:661 msgid ":meth:`Integration.edit` will edit an existing integration." msgstr ":meth:`Integration.edit` は既存のインテグレーションを編集します。" -#: ../../whats_new.rst:629 +#: ../../whats_new.rst:662 msgid ":meth:`Integration.delete` will delete an integration." msgstr ":meth:`Integration.delete` はインテグレーションを削除します。" -#: ../../whats_new.rst:630 +#: ../../whats_new.rst:663 msgid ":meth:`Integration.sync` will sync an integration." msgstr ":meth:`Integration.sync` はインテグレーションを同期します。" -#: ../../whats_new.rst:631 +#: ../../whats_new.rst:664 msgid "There is currently no support in the audit log for this." msgstr "これには現時点で監査ログのサポートはありません。" -#: ../../whats_new.rst:633 +#: ../../whats_new.rst:666 msgid "Add an alias for :attr:`VerificationLevel.extreme` under :attr:`VerificationLevel.very_high` (:issue:`2650`)" msgstr ":attr:`VerificationLevel.extreme` の別名を :attr:`VerificationLevel.very_high` の下に追加しました (:issue:`2650`)" -#: ../../whats_new.rst:634 +#: ../../whats_new.rst:667 msgid "Add various grey to gray aliases for :class:`Colour` (:issue:`5130`)" msgstr ":class:`Colour` に「グレー」の綴り違いのエイリアスを追加しました。 (:issue:`5130`)" -#: ../../whats_new.rst:635 +#: ../../whats_new.rst:668 msgid "Added :attr:`VoiceClient.latency` and :attr:`VoiceClient.average_latency` (:issue:`2535`)" msgstr ":attr:`VoiceClient.latency` と :attr:`VoiceClient.average_latency` を追加しました。 (:issue:`2535`)" -#: ../../whats_new.rst:636 +#: ../../whats_new.rst:669 msgid "Add ``use_cached`` and ``spoiler`` parameters to :meth:`Attachment.to_file` (:issue:`2577`, :issue:`4095`)" msgstr ":meth:`Attachment.to_file` にパラメータ ``use_cached`` と ``spoiler`` を追加しました。 (:issue:`2577`, :issue:`4095`)" -#: ../../whats_new.rst:637 +#: ../../whats_new.rst:670 msgid "Add ``position`` parameter support to :meth:`Guild.create_category` (:issue:`2623`)" msgstr ":meth:`Guild.create_category` にて ``position`` パラメータのサポートを追加しました。 (:issue:`2623`)" -#: ../../whats_new.rst:638 +#: ../../whats_new.rst:671 msgid "Allow passing ``int`` for the colour in :meth:`Role.edit` (:issue:`4057`)" msgstr ":meth:`Role.edit` のロールカラーに ``int`` が渡せるようになりました。 (:issue:`4057`)" -#: ../../whats_new.rst:639 +#: ../../whats_new.rst:672 msgid "Add :meth:`Embed.remove_author` to clear author information from an embed (:issue:`4068`)" msgstr "埋め込みの作者を削除する :meth:`Embed.remove_author` が追加されました。 (:issue:`4068`)" -#: ../../whats_new.rst:640 +#: ../../whats_new.rst:673 msgid "Add the ability to clear images and thumbnails in embeds using :attr:`Embed.Empty` (:issue:`4053`)" msgstr ":attr:`Embed.Empty` を使用してEmbed内のサムネイルと画像をクリアできるようになりました。 (:issue:`4053`)" -#: ../../whats_new.rst:641 +#: ../../whats_new.rst:674 msgid "Add :attr:`Guild.max_video_channel_users` (:issue:`4120`)" msgstr ":attr:`Guild.max_video_channel_users` を追加。( :issue:`4120` )" -#: ../../whats_new.rst:642 +#: ../../whats_new.rst:675 msgid "Add :attr:`Guild.public_updates_channel` (:issue:`4120`)" msgstr ":attr:`Guild.public_updates_channel` を追加。( :issue:`4120` )" -#: ../../whats_new.rst:643 +#: ../../whats_new.rst:676 msgid "Add ``guild_ready_timeout`` parameter to :class:`Client` and subclasses to control timeouts when the ``GUILD_CREATE`` stream takes too long (:issue:`4112`)" msgstr "``GUILD_CREATE`` に時間がかかりすぎるとき、タイムアウトをコントロールできように ``guild_ready_timeout`` パラメータを :class:`Client` に追加しました。 (:issue:`4112`)" -#: ../../whats_new.rst:644 +#: ../../whats_new.rst:677 msgid "Add support for public user flags via :attr:`User.public_flags` and :class:`PublicUserFlags` (:issue:`3999`)" msgstr ":attr:`User.public_flags` と :class:`PublicUserFlags` を介しユーザーフラグのサポートを追加しました。 (:issue:`3999`)" -#: ../../whats_new.rst:645 +#: ../../whats_new.rst:678 msgid "Allow changing of channel types via :meth:`TextChannel.edit` to and from a news channel (:issue:`4121`)" msgstr ":meth:`TextChannel.edit` を介してニュースチャンネルの種類を変更することができるようにしました。(:issue:`4121` )" -#: ../../whats_new.rst:646 +#: ../../whats_new.rst:679 msgid "Add :meth:`Guild.edit_role_positions` to bulk edit role positions in a single API call (:issue:`2501`, :issue:`2143`)" msgstr "一回のAPI呼び出しでロールの位置を一括変更できる :meth:`Guild.edit_role_positions` を追加しました。 (:issue:`2501`, :issue:`2143`)" -#: ../../whats_new.rst:647 +#: ../../whats_new.rst:680 msgid "Add :meth:`Guild.change_voice_state` to change your voice state in a guild (:issue:`5088`)" msgstr "ギルド内のボイスステートを変更する :meth:`Guild.change_voice_state` を追加しました。 (:issue:`5088`)" -#: ../../whats_new.rst:648 +#: ../../whats_new.rst:681 msgid "Add :meth:`PartialInviteGuild.is_icon_animated` for checking if the invite guild has animated icon (:issue:`4180`, :issue:`4181`)" msgstr "ギルドにアニメーションアイコンがあるか判断する :meth:`PartialInviteGuild.is_icon_animated` を追加しました。 (:issue:`4180`, :issue:`4181`)" -#: ../../whats_new.rst:649 +#: ../../whats_new.rst:682 msgid "Add :meth:`PartialInviteGuild.icon_url_as` now supports ``static_format`` for consistency (:issue:`4180`, :issue:`4181`)" msgstr "``static_format`` が :meth:`PartialInviteGuild.icon_url_as` に追加されました (:issue:`4180`, :issue:`4181`)" -#: ../../whats_new.rst:650 +#: ../../whats_new.rst:683 msgid "Add support for ``user_ids`` in :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` で、 ``user_ids`` 引数が使えるようになりました。" -#: ../../whats_new.rst:651 +#: ../../whats_new.rst:684 msgid "Add support for pruning members by roles in :meth:`Guild.prune_members` (:issue:`4043`)" msgstr ":meth:`Guild.prune_members` でメンバーをロールにより一括キックできるようになりました。 (:issue:`4043`)" -#: ../../whats_new.rst:652 +#: ../../whats_new.rst:685 msgid "|commands| Implement :func:`~ext.commands.before_invoke` and :func:`~ext.commands.after_invoke` decorators (:issue:`1986`, :issue:`2502`)" msgstr "|commands| :func:`~ext.commands.before_invoke` と :func:`~ext.commands.after_invoke` デコレーターを実装。 ( :issue:`1986`, :issue:`2502` )" -#: ../../whats_new.rst:653 +#: ../../whats_new.rst:686 msgid "|commands| Add a way to retrieve ``retry_after`` from a cooldown in a command via :meth:`Command.get_cooldown_retry_after <.ext.commands.Command.get_cooldown_retry_after>` (:issue:`5195`)" msgstr "|commands| :meth:`Command.get_cooldown_retry_after <.ext.commands.Command.get_cooldown_retry_after>` によってコマンド中のクールダウンから ``retry_after`` を取得する方法を追加しました (:issue:`5195`)" -#: ../../whats_new.rst:654 +#: ../../whats_new.rst:687 msgid "|commands| Add a way to dynamically add and remove checks from a :class:`HelpCommand <.ext.commands.HelpCommand>` (:issue:`5197`)" msgstr "|commands| :class:`HelpCommand <.ext.commands.HelpCommand>` から動的にチェックを追加したり削除したりする方法を追加しました (:issue:`5197`)" -#: ../../whats_new.rst:655 +#: ../../whats_new.rst:688 msgid "|tasks| Add :meth:`Loop.is_running <.ext.tasks.Loop.is_running>` method to the task objects (:issue:`2540`)" msgstr "|tasks| タスクオブジェクトに :meth:`Loop.is_running <.ext.tasks.Loop.is_running>` メソッドを追加しました (:issue:`2540`)" -#: ../../whats_new.rst:656 +#: ../../whats_new.rst:689 msgid "|tasks| Allow usage of custom error handlers similar to the command extensions to tasks using :meth:`Loop.error <.ext.tasks.Loop.error>` decorator (:issue:`2621`)" msgstr "|tasks| :meth:`Loop.error <.ext.tasks.Loop.error>` デコレーターを用いたタスクに対するコマンド拡張と同様のカスタムエラーハンドラーの使用を可能にしました (:issue:`2621`)" -#: ../../whats_new.rst:662 +#: ../../whats_new.rst:695 msgid "Fix issue with :attr:`PartialEmoji.url` reads leading to a failure (:issue:`4015`, :issue:`4016`)" msgstr ":attr:`PartialEmoji.url` での読み込みエラーを修正しました。 (:issue:`4015`, :issue:`4016`)" -#: ../../whats_new.rst:663 +#: ../../whats_new.rst:696 msgid "Allow :meth:`abc.Messageable.history` to take a limit of ``1`` even if ``around`` is passed (:issue:`4019`)" msgstr "``around`` が渡された場合でも、 :meth:`abc.Messageable.history` が上限 ``1`` を取ることができるようにしました。 (:issue:`4019`)" -#: ../../whats_new.rst:664 +#: ../../whats_new.rst:697 msgid "Fix :attr:`Guild.member_count` not updating in certain cases when a member has left the guild (:issue:`4021`)" msgstr "ギルドからメンバーが脱退したとき、特定の場合に :attr:`Guild.member_count` が更新されない問題を修正しました。 (:issue:`4021`)" -#: ../../whats_new.rst:665 +#: ../../whats_new.rst:698 msgid "Fix the type of :attr:`Object.id` not being validated. For backwards compatibility ``str`` is still allowed but is converted to ``int`` (:issue:`4002`)" msgstr ":attr:`Object.id` の型が検証されない問題を修正されました。後方互換性のため ``str`` は使用可能ですが、 ``int`` に変換されます。 (:issue:`4002`)" -#: ../../whats_new.rst:666 +#: ../../whats_new.rst:699 msgid "Fix :meth:`Guild.edit` not allowing editing of notification settings (:issue:`4074`, :issue:`4047`)" msgstr ":meth:`Guild.edit` で通知設定の編集ができない問題を修正しました。 (:issue:`4074`, :issue:`4047`)" -#: ../../whats_new.rst:667 +#: ../../whats_new.rst:700 msgid "Fix crash when the guild widget contains channels that aren't in the payload (:issue:`4114`, :issue:`4115`)" msgstr "ギルドウィジェットの中にペイロードにないチャンネルが含まれている場合にクラッシュする問題を修正しました。 (:issue:`4114`, :issue:`4115`)" -#: ../../whats_new.rst:668 +#: ../../whats_new.rst:701 msgid "Close ffmpeg stdin handling from spawned processes with :class:`FFmpegOpusAudio` and :class:`FFmpegPCMAudio` (:issue:`4036`)" msgstr ":class:`FFmpegOpusAudio` および :class:`FFmpegPCMAudio` を使って生成されたプロセスからの ffmpeg stdin のハンドリングを閉じるようにしました (:issue:`4036`)" -#: ../../whats_new.rst:669 +#: ../../whats_new.rst:702 msgid "Fix :func:`utils.escape_markdown` not escaping masked links (:issue:`4206`, :issue:`4207`)" msgstr ":func:`utils.escape_markdown` がマスクされたリンクをエスケープしない問題を修正しました。 (:issue:`4206`, :issue:`4207`)" -#: ../../whats_new.rst:670 +#: ../../whats_new.rst:703 msgid "Fix reconnect loop due to failed handshake on region change (:issue:`4210`, :issue:`3996`)" msgstr "リージョン変更時のハンドシェイクの失敗による再接続のループを修正しました (:issue:`4210`, :issue:`3996`)" -#: ../../whats_new.rst:671 +#: ../../whats_new.rst:704 msgid "Fix :meth:`Guild.by_category` not returning empty categories (:issue:`4186`)" msgstr "空のカテゴリーを返さない :meth:`Guild.by_category` を修正しました (:issue:`4186`)" -#: ../../whats_new.rst:672 +#: ../../whats_new.rst:705 msgid "Fix certain JPEG images not being identified as JPEG (:issue:`5143`)" msgstr "特定の JPEG 画像が JPEG として認識されないのを修正 (:issue:`5143`)" -#: ../../whats_new.rst:673 +#: ../../whats_new.rst:706 msgid "Fix a crash when an incomplete guild object is used when fetching reaction information (:issue:`5181`)" msgstr "反応情報を取得する際に不完全なギルドオブジェクトを使用するとクラッシュする問題を修正しました (:issue:`5181`)" -#: ../../whats_new.rst:674 +#: ../../whats_new.rst:707 msgid "Fix a timeout issue when fetching members using :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` を使用してメンバーを取得する際のタイムアウトの問題を修正しました。" -#: ../../whats_new.rst:675 +#: ../../whats_new.rst:708 msgid "Fix an issue with domain resolution in voice (:issue:`5188`, :issue:`5191`)" msgstr "音声のドメイン解決に関する問題を修正しました (:issue:`5188`, :issue:`5191`)" -#: ../../whats_new.rst:676 +#: ../../whats_new.rst:709 msgid "Fix an issue where :attr:`PartialEmoji.id` could be a string (:issue:`4153`, :issue:`4152`)" msgstr ":attr:`PartialEmoji.id` が文字列である可能性がある問題を修正しました (:issue:`4153`, :issue:`4152`)" -#: ../../whats_new.rst:677 +#: ../../whats_new.rst:710 msgid "Fix regression where :attr:`Member.activities` would not clear." msgstr ":attr:`Member.activities` がクリアされないリグレッションを修正しました。" -#: ../../whats_new.rst:678 +#: ../../whats_new.rst:711 msgid "|commands| A :exc:`TypeError` is now raised when :obj:`typing.Optional` is used within :data:`commands.Greedy <.ext.commands.Greedy>` (:issue:`2253`, :issue:`5068`)" msgstr "|commands| :data:`commands.Greedy <.ext.commands.Greedy>` 内で :obj:`typing.Optional` を使用すると :exc:`TypeError` が発生します (:issue:`2253`, :issue:`5068`)." -#: ../../whats_new.rst:679 +#: ../../whats_new.rst:712 msgid "|commands| :meth:`Bot.walk_commands <.ext.commands.Bot.walk_commands>` no longer yields duplicate commands due to aliases (:issue:`2591`)" msgstr "|commands| :meth:`Bot.walk_commands <.ext.commands.Bot.walk_commands>` はエイリアスにより重複したコマンドを生成しないようになりました (:issue:`2591`)" -#: ../../whats_new.rst:680 +#: ../../whats_new.rst:713 msgid "|commands| Fix regex characters not being escaped in :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` (:issue:`4058`, :issue:`4071`)" msgstr "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` で正規化されていない文字を修正しました (:issue:`4058`, :issue:`4071`)" -#: ../../whats_new.rst:681 +#: ../../whats_new.rst:714 msgid "|commands| Fix :meth:`Bot.get_command <.ext.commands.Bot.get_command>` from raising errors when a name only has whitespace (:issue:`5124`)" msgstr "|commands| 名前に空白文字しかない場合にエラーを発生させないように :meth:`Bot.get_command <.ext.commands.Bot.get_command>` を修正しました (:issue:`5124`)" -#: ../../whats_new.rst:682 +#: ../../whats_new.rst:715 msgid "|commands| Fix issue with :attr:`Context.subcommand_passed <.ext.commands.Context.subcommand_passed>` not functioning as expected (:issue:`5198`)" msgstr "|commands| :attr:`Context.subcommand_passed <.ext.commands.Context.subcommand_passed>` が期待通りに機能しない問題を修正しました (:issue:`5198`)" -#: ../../whats_new.rst:683 +#: ../../whats_new.rst:716 msgid "|tasks| Task objects are no longer stored globally so two class instances can now start two separate tasks (:issue:`2294`)" msgstr "|tasks| Task objects are no longer stored globally so two class instances can start two separate tasks (:issue:`2294`)" -#: ../../whats_new.rst:684 +#: ../../whats_new.rst:717 msgid "|tasks| Allow cancelling the loop within :meth:`before_loop <.ext.tasks.Loop.before_loop>` (:issue:`4082`)" msgstr "|tasks| 内のループをキャンセルできるようにする。:meth:`before_loop <.ext.tasks.Loop.before_loop>` (:issue:`4082`)" -#: ../../whats_new.rst:690 +#: ../../whats_new.rst:723 msgid "The :attr:`Member.roles` cache introduced in v1.3 was reverted due to issues caused (:issue:`4087`, :issue:`4157`)" msgstr "v1.3 で導入された :attr:`Member.roles` キャッシュは、問題が発生したため元に戻されました (:issue:`4087`, :issue:`4157`)" -#: ../../whats_new.rst:691 +#: ../../whats_new.rst:724 msgid ":class:`Webhook` objects are now comparable and hashable (:issue:`4182`)" msgstr ":class:`Webhook` オブジェクトが比較可能になり、ハッシュ化できるようになりました (:issue:`4182`)" -#: ../../whats_new.rst:695 +#: ../../whats_new.rst:728 msgid "Some more API requests got a ``reason`` parameter for audit logs (:issue:`5086`)" msgstr "さらにいくつかの API リクエストで、監査ログ用の ``reason`` パラメータが取得されました (:issue:`5086`)" -#: ../../whats_new.rst:693 +#: ../../whats_new.rst:726 msgid ":meth:`TextChannel.follow`" msgstr ":meth:`TextChannel.follow`" -#: ../../whats_new.rst:694 +#: ../../whats_new.rst:727 msgid ":meth:`Message.pin` and :meth:`Message.unpin`" msgstr ":meth:`Message.pin` と :meth:`Message.unpin`" -#: ../../whats_new.rst:695 +#: ../../whats_new.rst:728 msgid ":meth:`Webhook.delete` and :meth:`Webhook.edit`" msgstr ":meth:`Webhook.delete` と :meth:`Webhook.edit`" -#: ../../whats_new.rst:697 +#: ../../whats_new.rst:730 msgid "For performance reasons ``websockets`` has been dropped in favour of ``aiohttp.ws``." msgstr "パフォーマンス上の理由から、 ``websockets`` は削除され、 ``aiohttp.ws`` が使用されるようになりました。" -#: ../../whats_new.rst:698 +#: ../../whats_new.rst:731 msgid "The blocking logging message now shows the stack trace of where the main thread was blocking" msgstr "ブロッキングのログメッセージは、メインスレッドがブロッキングしていた場所のスタックトレースを表示するようになりました" -#: ../../whats_new.rst:699 +#: ../../whats_new.rst:732 msgid "The domain name was changed from ``discordapp.com`` to ``discord.com`` to prepare for the required domain migration" msgstr "必要なドメイン移行の準備のため、ドメイン名を ``discordapp.com`` から ``discord.com`` に変更しました。" -#: ../../whats_new.rst:700 +#: ../../whats_new.rst:733 msgid "Reduce memory usage when reconnecting due to stale references being held by the message cache (:issue:`5133`)" msgstr "メッセージキャッシュに保持されている古い参照による再接続時のメモリ使用量を削減しました (:issue:`5133`)" -#: ../../whats_new.rst:701 +#: ../../whats_new.rst:734 msgid "Optimize :meth:`abc.GuildChannel.permissions_for` by not creating as many temporary objects (20-32% savings)." msgstr "テンポラリオブジェクトをあまり作成しないように :meth:`abc.GuildChannel.permissions_for` を最適化しました (20-32%の節約)。" -#: ../../whats_new.rst:702 +#: ../../whats_new.rst:735 msgid "|commands| Raise :exc:`~ext.commands.CommandRegistrationError` instead of :exc:`ClientException` when a duplicate error is registered (:issue:`4217`)" msgstr "|commands| 重複するエラーが登録された場合、 :exc:`ClientException` ではなく :exc:`~ext.commands.CommandRegistrationError` を発生するようにしました (:issue:`4217`)" -#: ../../whats_new.rst:703 +#: ../../whats_new.rst:736 msgid "|tasks| No longer handle :exc:`HTTPException` by default in the task reconnect loop (:issue:`5193`)" msgstr "|tasks| タスクの再接続ループにおいて、デフォルトで :exc:`HTTPException` を処理しないようにしました (:issue:`5193`)" -#: ../../whats_new.rst:708 +#: ../../whats_new.rst:741 msgid "v1.3.4" msgstr "v1.3.4" -#: ../../whats_new.rst:713 +#: ../../whats_new.rst:746 msgid "Fix an issue with channel overwrites causing multiple issues including crashes (:issue:`5109`)" msgstr "チャンネルの上書きがクラッシュを含む複数の問題を引き起こす問題を修正しました (:issue:`5109`)" -#: ../../whats_new.rst:718 +#: ../../whats_new.rst:751 msgid "v1.3.3" msgstr "v1.3.3" -#: ../../whats_new.rst:724 +#: ../../whats_new.rst:757 msgid "Change default WS close to 4000 instead of 1000." msgstr "デフォルトのWSクローズを1000から4000に変更。" -#: ../../whats_new.rst:724 +#: ../../whats_new.rst:757 msgid "The previous close code caused sessions to be invalidated at a higher frequency than desired." msgstr "以前のクローズコードは、望ましい頻度よりも高い頻度でセッションが無効化される原因となっていました。" -#: ../../whats_new.rst:726 +#: ../../whats_new.rst:759 msgid "Fix ``None`` appearing in ``Member.activities``. (:issue:`2619`)" msgstr "``Member.activities`` に表示される ``None`` を修正しました。(:issue:`2619`)" -#: ../../whats_new.rst:731 +#: ../../whats_new.rst:764 msgid "v1.3.2" msgstr "v1.3.2" -#: ../../whats_new.rst:733 +#: ../../whats_new.rst:766 msgid "Another minor bug fix release." msgstr "もう一つのマイナーなバグフィックスリリースです。" -#: ../../whats_new.rst:738 +#: ../../whats_new.rst:771 msgid "Higher the wait time during the ``GUILD_CREATE`` stream before ``on_ready`` is fired for :class:`AutoShardedClient`." msgstr ":class:`AutoShardedClient` の ``GUILD_CREATE`` ストリームで ``on_ready`` が発生するまでの待ち時間を長くするようにしました。" -#: ../../whats_new.rst:739 +#: ../../whats_new.rst:772 msgid ":func:`on_voice_state_update` now uses the inner ``member`` payload which should make it more reliable." msgstr ":func:`on_voice_state_update` は内側の ``member`` ペイロードを使用するようになり、より信頼性が高くなりました。" -#: ../../whats_new.rst:740 +#: ../../whats_new.rst:773 msgid "Fix various Cloudflare handling errors (:issue:`2572`, :issue:`2544`)" msgstr "Cloudflare のハンドリングエラーを修正しました (:issue:`2572`, :issue:`2544`)" -#: ../../whats_new.rst:741 +#: ../../whats_new.rst:774 msgid "Fix crashes if :attr:`Message.guild` is :class:`Object` instead of :class:`Guild`." msgstr ":attr:`Message.guild` が :class:`Guild` ではなく :class:`Object` であった場合のクラッシュを修正しました。" -#: ../../whats_new.rst:742 +#: ../../whats_new.rst:775 msgid "Fix :meth:`Webhook.send` returning an empty string instead of ``None`` when ``wait=False``." msgstr ":meth:`Webhook.send` が ``wait=False`` の時に ``None`` ではなく空の文字列を返すように修正しました。" -#: ../../whats_new.rst:743 +#: ../../whats_new.rst:776 msgid "Fix invalid format specifier in webhook state (:issue:`2570`)" msgstr "Webhook の状態における無効なフォーマット指定子を修正 (:issue:`2570`)" -#: ../../whats_new.rst:744 +#: ../../whats_new.rst:777 msgid "|commands| Passing invalid permissions to permission related checks now raises ``TypeError``." msgstr "|commands| パーミッション関連のチェックで無効なパーミッションを渡すと ``TypeError`` が発生するようになりました。" -#: ../../whats_new.rst:749 +#: ../../whats_new.rst:782 msgid "v1.3.1" msgstr "v1.3.1" -#: ../../whats_new.rst:751 +#: ../../whats_new.rst:784 msgid "Minor bug fix release." msgstr "マイナーなバグフィックスリリースです。" -#: ../../whats_new.rst:756 +#: ../../whats_new.rst:789 msgid "Fix fetching invites in guilds that the user is not in." msgstr "ユーザーが参加していないギルドの招待状をフェッチするように修正しました。" -#: ../../whats_new.rst:757 +#: ../../whats_new.rst:790 msgid "Fix the channel returned from :meth:`Client.fetch_channel` raising when sending messages. (:issue:`2531`)" msgstr "メッセージ送信時に :meth:`Client.fetch_channel` から返されるチャンネルを修正しました。(:issue:`2531`)" -#: ../../whats_new.rst:762 +#: ../../whats_new.rst:795 msgid "Fix compatibility warnings when using the Python 3.9 alpha." msgstr "Python 3.9 alpha を使用する際の互換性警告を修正。" -#: ../../whats_new.rst:763 +#: ../../whats_new.rst:796 msgid "Change the unknown event logging from WARNING to DEBUG to reduce noise." msgstr "ノイズを減らすために、不明なイベントのログをWARNINGからDEBUGに変更します。" -#: ../../whats_new.rst:768 +#: ../../whats_new.rst:801 msgid "v1.3.0" msgstr "v1.3.0" -#: ../../whats_new.rst:770 +#: ../../whats_new.rst:803 msgid "This version comes with a lot of bug fixes and new features. It's been in development for a lot longer than was anticipated!" msgstr "このバージョンでは、多くのバグフィックスと新機能が搭載されています。予想以上に長い期間、開発が続けられているのです!" -#: ../../whats_new.rst:775 +#: ../../whats_new.rst:808 msgid "Add :meth:`Guild.fetch_members` to fetch members from the HTTP API. (:issue:`2204`)" msgstr "HTTP API からメンバーを取得するための :meth:`Guild.fetch_members` を追加しました。(:issue:`2204`)" -#: ../../whats_new.rst:776 +#: ../../whats_new.rst:809 msgid "Add :meth:`Guild.fetch_roles` to fetch roles from the HTTP API. (:issue:`2208`)" msgstr "HTTP API からロールをフェッチするために :meth:`Guild.fetch_roles` を追加しました。(:issue:`2208`)" -#: ../../whats_new.rst:777 +#: ../../whats_new.rst:810 msgid "Add support for teams via :class:`Team` when fetching with :meth:`Client.application_info`. (:issue:`2239`)" msgstr ":meth:`Client.application_info` で取得する際に、 :class:`Team` を介してチームをサポートする機能を追加しました。(:issue:`2239`)" -#: ../../whats_new.rst:778 +#: ../../whats_new.rst:811 msgid "Add support for suppressing embeds via :meth:`Message.edit`" msgstr ":meth:`Message.edit` による埋め込みの抑制をサポートするようにしました。" -#: ../../whats_new.rst:779 +#: ../../whats_new.rst:812 msgid "Add support for guild subscriptions. See the :class:`Client` documentation for more details." msgstr "ギルドサブスクリプションのサポートを追加しました。詳細は :class:`Client` のドキュメントを参照してください。" -#: ../../whats_new.rst:780 +#: ../../whats_new.rst:813 msgid "Add :attr:`VoiceChannel.voice_states` to get voice states without relying on member cache." msgstr "メンバーキャッシュに依存せずに音声の状態を取得するために、 :attr:`VoiceChannel.voice_states` を追加しました。" -#: ../../whats_new.rst:781 +#: ../../whats_new.rst:814 msgid "Add :meth:`Guild.query_members` to request members from the gateway." msgstr "ゲートウェイにメンバーを要求するために :meth:`Guild.query_members` を追加しました。" -#: ../../whats_new.rst:782 +#: ../../whats_new.rst:815 msgid "Add :class:`FFmpegOpusAudio` and other voice improvements. (:issue:`2258`)" msgstr ":class:`FFmpegOpusAudio` を追加し、その他の音声の改良を行いました。(:issue:`2258`)" -#: ../../whats_new.rst:783 +#: ../../whats_new.rst:816 msgid "Add :attr:`RawMessageUpdateEvent.channel_id` for retrieving channel IDs during raw message updates. (:issue:`2301`)" msgstr "Rawメッセージの更新時にチャンネルIDを取得するための :attr:`RawMessageUpdateEvent.channel_id` を追加しました。(:issue:`2301`)" -#: ../../whats_new.rst:784 +#: ../../whats_new.rst:817 msgid "Add :attr:`RawReactionActionEvent.event_type` to disambiguate between reaction addition and removal in reaction events." msgstr "リアクションイベントでリアクションが追加されたか除去されたかを明確にする :attr:`RawReactionActionEvent.event_type` を追加しました。" -#: ../../whats_new.rst:785 +#: ../../whats_new.rst:818 msgid "Add :attr:`abc.GuildChannel.permissions_synced` to query whether permissions are synced with the category. (:issue:`2300`, :issue:`2324`)" msgstr "権限がカテゴリと同期されているかを確認する :attr:`abc.GuildChannel.permissions_synced` を追加しました。 (:issue:`2300`, :issue:`2324`)" -#: ../../whats_new.rst:786 +#: ../../whats_new.rst:819 msgid "Add :attr:`MessageType.channel_follow_add` message type for announcement channels being followed. (:issue:`2314`)" msgstr "フォローされているアナウンスチャンネル用の :attr:`MessageType.channel_follow_add` メッセージタイプを追加しました。(:issue:`2314`)" -#: ../../whats_new.rst:787 +#: ../../whats_new.rst:820 msgid "Add :meth:`Message.is_system` to allow for quickly filtering through system messages." msgstr "システムメッセージを素早くフィルタリングできるように :meth:`Message.is_system` を追加しました。" -#: ../../whats_new.rst:788 +#: ../../whats_new.rst:821 msgid "Add :attr:`VoiceState.self_stream` to indicate whether someone is streaming via Go Live. (:issue:`2343`)" msgstr "誰かがGo Live経由でストリーミングしているかどうかを示すための、 :attr:`VoiceState.self_stream` を追加しました。 (:issue:`2343`)" -#: ../../whats_new.rst:789 +#: ../../whats_new.rst:822 msgid "Add :meth:`Emoji.is_usable` to check if the client user can use an emoji. (:issue:`2349`)" msgstr "クライアントユーザーが絵文字を使用できるかどうかを確認できるように、 :meth:`Emoji.is_usable` を追加しました。 (:issue:`2349`)" -#: ../../whats_new.rst:790 +#: ../../whats_new.rst:823 msgid "Add :attr:`VoiceRegion.europe` and :attr:`VoiceRegion.dubai`. (:issue:`2358`, :issue:`2490`)" msgstr ":attr:`VoiceRegion.europe` と :attr:`VoiceRegion.dubai` を追加しました。 (:issue:`2358`, :issue:`2490`)" -#: ../../whats_new.rst:791 +#: ../../whats_new.rst:824 msgid "Add :meth:`TextChannel.follow` to follow a news channel. (:issue:`2367`)" msgstr "ニュースチャンネルをフォローする :meth:`TextChannel.follow` を追加しました。 (:issue:`2367`)" -#: ../../whats_new.rst:792 +#: ../../whats_new.rst:825 msgid "Add :attr:`Permissions.view_guild_insights` permission. (:issue:`2415`)" msgstr ":attr:`Permissions.view_guild_insights` 権限を追加しました。 (:issue:`2415`)" -#: ../../whats_new.rst:794 +#: ../../whats_new.rst:827 msgid "Add support for new audit log types. See :ref:`discord-api-audit-logs` for more information. (:issue:`2427`)" msgstr "新しい監査ログタイプのサポートを追加しました。詳細については :ref:`discord-api-audit-logs` を参照してください。 (:issue:`2427`)" -#: ../../whats_new.rst:794 +#: ../../whats_new.rst:827 msgid "Note that integration support is not finalized." msgstr "インテグレーションのサポートは未確定であることに注意してください。" -#: ../../whats_new.rst:796 +#: ../../whats_new.rst:829 msgid "Add :attr:`Webhook.type` to query the type of webhook (:class:`WebhookType`). (:issue:`2441`)" msgstr "ウェブフック( :class:`WebhookType` )の種類を問い合わせるための :attr:`Webhook.type` を追加しました。 (:issue:`2441`)" -#: ../../whats_new.rst:797 +#: ../../whats_new.rst:830 msgid "Allow bulk editing of channel overwrites through :meth:`abc.GuildChannel.edit`. (:issue:`2198`)" msgstr "チャンネル上書きの一括編集を :meth:`abc.GuildChannel.edit` を通して行えるようにしました。(:issue:`2198`)" -#: ../../whats_new.rst:798 +#: ../../whats_new.rst:831 msgid "Add :class:`Activity.created_at` to see when an activity was started. (:issue:`2446`)" msgstr "アクティビティがいつ開始されたかを確認するために :class:`Activity.created_at` を追加しました。(:issue:`2446`)" -#: ../../whats_new.rst:799 +#: ../../whats_new.rst:832 msgid "Add support for ``xsalsa20_poly1305_lite`` encryption mode for voice. (:issue:`2463`)" msgstr "音声用の ``xsalsa20_poly1305_lite`` 暗号化モードのサポートを追加しました。(:issue:`2463`)" -#: ../../whats_new.rst:800 +#: ../../whats_new.rst:833 msgid "Add :attr:`RawReactionActionEvent.member` to get the member who did the reaction. (:issue:`2443`)" msgstr "リアクションを行ったメンバーを取得するために :attr:`RawReactionActionEvent.member` を追加しました。(:issue:`2443`)" -#: ../../whats_new.rst:801 +#: ../../whats_new.rst:834 msgid "Add support for new YouTube streaming via :attr:`Streaming.platform` and :attr:`Streaming.game`. (:issue:`2445`)" msgstr ":attr:`Streaming.platform` と :attr:`Streaming.game` による新しい YouTube ストリーミングのサポートを追加しました。(:issue:`2445`)" -#: ../../whats_new.rst:802 +#: ../../whats_new.rst:835 msgid "Add :attr:`Guild.discovery_splash_url` to get the discovery splash image asset. (:issue:`2482`)" msgstr "ディスカバリースプラッシュイメージアセットを取得するために :attr:`Guild.discovery_splash_url` を追加しました。(:issue:`2482`)" -#: ../../whats_new.rst:804 +#: ../../whats_new.rst:837 msgid "Add :attr:`Guild.rules_channel` to get the rules channel of public guilds. (:issue:`2482`)" msgstr "パブリック・ギルドのルール・チャンネルを取得するために :attr:`Guild.rules_channel` を追加しました。(:issue:`2482`)" -#: ../../whats_new.rst:804 +#: ../../whats_new.rst:837 msgid "It should be noted that this feature is restricted to those who are either in Server Discovery or planning to be there." msgstr "なお、この機能はサーバーディスカバリーに参加されている方、または参加予定の方に限定しています。" -#: ../../whats_new.rst:806 +#: ../../whats_new.rst:839 msgid "Add support for message flags via :attr:`Message.flags` and :class:`MessageFlags`. (:issue:`2433`)" msgstr ":attr:`Message.flags` と :class:`MessageFlags` によるメッセージフラグのサポートを追加しました。(:issue:`2433`)" -#: ../../whats_new.rst:807 +#: ../../whats_new.rst:840 msgid "Add :attr:`User.system` and :attr:`Profile.system` to know whether a user is an official Discord Trust and Safety account." msgstr "ユーザーがDiscord Trust and Safetyの公式アカウントであるかどうかを知るために、 :attr:`User.system` と :attr:`Profile.system` を追加しました。" -#: ../../whats_new.rst:808 +#: ../../whats_new.rst:841 msgid "Add :attr:`Profile.team_user` to check whether a user is a member of a team." msgstr "ユーザーがチームのメンバーであるかどうかを確認するために :attr:`Profile.team_user` を追加しました。" -#: ../../whats_new.rst:809 +#: ../../whats_new.rst:842 msgid "Add :meth:`Attachment.to_file` to easily convert attachments to :class:`File` for sending." msgstr "添付ファイルを簡単に :class:`File` に変換して送信できるように :meth:`Attachment.to_file` を追加。" -#: ../../whats_new.rst:813 +#: ../../whats_new.rst:846 msgid "Add certain aliases to :class:`Permissions` to match the UI better. (:issue:`2496`)" msgstr "UIにマッチするように、特定のエイリアスを :class:`Permissions` に追加しました。(:issue:`2496`)" -#: ../../whats_new.rst:811 +#: ../../whats_new.rst:844 msgid ":attr:`Permissions.manage_permissions`" msgstr ":attr:`Permissions.manage_permissions`" -#: ../../whats_new.rst:812 +#: ../../whats_new.rst:845 msgid ":attr:`Permissions.view_channel`" msgstr ":attr:`Permissions.view_channel`" -#: ../../whats_new.rst:813 +#: ../../whats_new.rst:846 msgid ":attr:`Permissions.use_external_emojis`" msgstr ":attr:`Permissions.use_external_emojis`" -#: ../../whats_new.rst:815 +#: ../../whats_new.rst:848 msgid "Add support for passing keyword arguments when creating :class:`Permissions`." msgstr ":class:`Permissions` を作成する際に、キーワード引数を渡せるようになりました。" -#: ../../whats_new.rst:817 +#: ../../whats_new.rst:850 msgid "Add support for custom activities via :class:`CustomActivity`. (:issue:`2400`)" msgstr ":class:`CustomActivity` によるカスタムアクティビティーのサポートを追加しました。(:issue:`2400`)" -#: ../../whats_new.rst:817 +#: ../../whats_new.rst:850 msgid "Note that as of now, bots cannot send custom activities yet." msgstr "なお、現在のところ、ボットはまだカスタムアクティビティを送信できません。" -#: ../../whats_new.rst:819 +#: ../../whats_new.rst:852 msgid "Add support for :func:`on_invite_create` and :func:`on_invite_delete` events." msgstr ":func:`on_invite_create` と :func:`on_invite_delete` イベントのサポートを追加しました。" -#: ../../whats_new.rst:822 +#: ../../whats_new.rst:855 msgid "Add support for clearing a specific reaction emoji from a message." msgstr "メッセージから特定のリアクション絵文字を消去する機能を追加しました。" -#: ../../whats_new.rst:821 +#: ../../whats_new.rst:854 msgid ":meth:`Message.clear_reaction` and :meth:`Reaction.clear` methods." msgstr ":meth:`Message.clear_reaction` および :meth:`Reaction.clear` メソッドを使用します。" -#: ../../whats_new.rst:822 +#: ../../whats_new.rst:855 msgid ":func:`on_raw_reaction_clear_emoji` and :func:`on_reaction_clear_emoji` events." msgstr ":func:`on_raw_reaction_clear_emoji` と :func:`on_reaction_clear_emoji` イベントです。" -#: ../../whats_new.rst:824 +#: ../../whats_new.rst:857 msgid "Add :func:`utils.sleep_until` helper to sleep until a specific datetime. (:issue:`2517`, :issue:`2519`)" msgstr "特定の日付までスリープさせる :func:`utils.sleep_until` ヘルパーを追加しました。(:issue:`2517`、:issue:`2519`)" -#: ../../whats_new.rst:825 +#: ../../whats_new.rst:858 msgid "|commands| Add support for teams and :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` to have multiple bot owners. (:issue:`2239`)" msgstr "|commands| チームと :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` が複数のボットオーナーを持つためのサポートを追加しました。(:issue:`2239`)" -#: ../../whats_new.rst:826 +#: ../../whats_new.rst:859 msgid "|commands| Add new :attr:`BucketType.role <.ext.commands.BucketType.role>` bucket type. (:issue:`2201`)" msgstr "|commands| 新しい :attr:`BucketType.role <.ext.commands.BucketType.role>` のバケットタイプを追加しました。(:issue:`2201`)です。" -#: ../../whats_new.rst:827 +#: ../../whats_new.rst:860 msgid "|commands| Expose :attr:`Command.cog <.ext.commands.Command.cog>` property publicly. (:issue:`2360`)" msgstr "|commands| :attr:`Command.cog <.ext.commands.Command.cog>` のプロパティを公開します。(:issue:`2360`)" -#: ../../whats_new.rst:828 +#: ../../whats_new.rst:861 msgid "|commands| Add non-decorator interface for adding checks to commands via :meth:`Command.add_check <.ext.commands.Command.add_check>` and :meth:`Command.remove_check <.ext.commands.Command.remove_check>`. (:issue:`2411`)" msgstr "|commands| :meth:`Command.add_check <.ext.commands.Command.add_check>` および :meth:`Command.remove_check <.ext.commands.Command.remove_check>` によりコマンドにチェックを追加する非デコレーターインターフェイスを追加しました。(:issue:`2411`)" -#: ../../whats_new.rst:829 +#: ../../whats_new.rst:862 msgid "|commands| Add :func:`has_guild_permissions <.ext.commands.has_guild_permissions>` check. (:issue:`2460`)" msgstr "|commands| :func:`has_guild_permissions <.ext.commands.has_guild_permissions>` のチェックを追加しました。(:issue:`2460`)" -#: ../../whats_new.rst:830 +#: ../../whats_new.rst:863 msgid "|commands| Add :func:`bot_has_guild_permissions <.ext.commands.bot_has_guild_permissions>` check. (:issue:`2460`)" msgstr "|commands| :func:`has_guild_permissions <.ext.commands.bot_has_guild_permissions>` のチェックを追加しました。(:issue:`2460`)" -#: ../../whats_new.rst:831 +#: ../../whats_new.rst:864 msgid "|commands| Add ``predicate`` attribute to checks decorated with :func:`~.ext.commands.check`." msgstr "|commands| :func:`~.ext.commands.check` で装飾されたチェックに ``predicate`` 属性を追加しました。" -#: ../../whats_new.rst:832 +#: ../../whats_new.rst:865 msgid "|commands| Add :func:`~.ext.commands.check_any` check to logical OR multiple checks." msgstr "|commands| :func:`~.ext.commands.check_any` チェックを論理的 OR 複数のチェックに追加しました。" -#: ../../whats_new.rst:833 +#: ../../whats_new.rst:866 msgid "|commands| Add :func:`~.ext.commands.max_concurrency` to allow only a certain amount of users to use a command concurrently before waiting or erroring." msgstr "|commands| 待ち時間やエラーになる前に、ある一定のユーザーだけがコマンドを同時に使用できるようにするための :func:`~.ext.commands.max_concurrency` を追加しました。" -#: ../../whats_new.rst:834 +#: ../../whats_new.rst:867 msgid "|commands| Add support for calling a :class:`~.ext.commands.Command` as a regular function." msgstr "|commands| :class:`~.ext.commands.Command` を通常の関数として呼び出すためのサポートを追加しました。" -#: ../../whats_new.rst:835 +#: ../../whats_new.rst:868 msgid "|tasks| :meth:`Loop.add_exception_type <.ext.tasks.Loop.add_exception_type>` now allows multiple exceptions to be set. (:issue:`2333`)" msgstr "|tasks| :meth:`Loop.add_exception_type <.ext.tasks.Loop.add_exception_type>` が、複数の例外を設定できるようになりました。(:issue:`2333`)" -#: ../../whats_new.rst:836 +#: ../../whats_new.rst:869 msgid "|tasks| Add :attr:`Loop.next_iteration <.ext.tasks.Loop.next_iteration>` property. (:issue:`2305`)" msgstr "|tasks| Add :attr:`Loop.next_iteration <.ext.tasks.Loop.next_iteration>` プロパティを追加しました。(:issue:`2305`)" -#: ../../whats_new.rst:841 +#: ../../whats_new.rst:874 msgid "Fix issue with permission resolution sometimes failing for guilds with no owner." msgstr "所有者がいないギルドで権限解決に失敗することがある問題を修正しました。" -#: ../../whats_new.rst:842 +#: ../../whats_new.rst:875 msgid "Tokens are now stripped upon use. (:issue:`2135`)" msgstr "トークンは、使用時に剥奪されるようになりました。(:issue:`2135`)" -#: ../../whats_new.rst:843 +#: ../../whats_new.rst:876 msgid "Passing in a ``name`` is no longer required for :meth:`Emoji.edit`. (:issue:`2368`)" msgstr ":meth:`Emoji.edit` に ``name`` を渡す必要はなくなりました。(:issue:`2368`)" -#: ../../whats_new.rst:844 +#: ../../whats_new.rst:877 msgid "Fix issue with webhooks not re-raising after retries have run out. (:issue:`2272`, :issue:`2380`)" msgstr "Webhooks がリトライを使い切った後に再レイズしない問題を修正しました。(:issue:`2272`, :issue:`2380`)" -#: ../../whats_new.rst:845 +#: ../../whats_new.rst:878 msgid "Fix mismatch in URL handling in :func:`utils.escape_markdown`. (:issue:`2420`)" msgstr ":func:`utils.escape_markdown` のURLハンドリングにおけるミスマッチを修正しました。(:issue:`2420`)" -#: ../../whats_new.rst:846 +#: ../../whats_new.rst:879 msgid "Fix issue with ports being read in little endian when they should be big endian in voice connections. (:issue:`2470`)" msgstr "音声接続において、ビッグエンディアンであるべきポートがリトルエンディアンで読み込まれる問題を修正しました。(:issue:`2470`)" -#: ../../whats_new.rst:847 +#: ../../whats_new.rst:880 msgid "Fix :meth:`Member.mentioned_in` not taking into consideration the message's guild." msgstr "メッセージのギルドが考慮されない :meth:`Member.mentioned_in` を修正しました。" -#: ../../whats_new.rst:848 +#: ../../whats_new.rst:881 msgid "Fix bug with moving channels when there are gaps in positions due to channel deletion and creation." msgstr "チャンネルの削除と作成によりポジションにギャップがある場合、チャンネルを移動する不具合を修正。" -#: ../../whats_new.rst:849 +#: ../../whats_new.rst:882 msgid "Fix :func:`on_shard_ready` not triggering when ``fetch_offline_members`` is disabled. (:issue:`2504`)" msgstr "``fetch_offline_members`` が無効の場合、 :func:`on_shard_ready` が発火されない問題を修正しました。(:issue:`2504`)" -#: ../../whats_new.rst:850 +#: ../../whats_new.rst:883 msgid "Fix issue with large sharded bots taking too long to actually dispatch :func:`on_ready`." msgstr "シャードを使用している大きなBotが :func:`on_ready` を実際に発火するのに長い時間を掛けていた問題を修正しました。" -#: ../../whats_new.rst:851 +#: ../../whats_new.rst:884 msgid "Fix issue with fetching group DM based invites in :meth:`Client.fetch_invite`." msgstr ":meth:`Client.fetch_invite` でグループDMベースの招待を取得する際の問題を修正しました。" -#: ../../whats_new.rst:852 +#: ../../whats_new.rst:885 msgid "Fix out of order files being sent in webhooks when there are 10 files." msgstr "10つのファイルをWebhookで送信する際、ファイルの順序が狂う問題を修正しました。" -#: ../../whats_new.rst:853 +#: ../../whats_new.rst:886 msgid "|commands| Extensions that fail internally due to ImportError will no longer raise :exc:`~.ext.commands.ExtensionNotFound`. (:issue:`2244`, :issue:`2275`, :issue:`2291`)" msgstr "|commands| ImportErrorによって内部的に失敗する拡張機能は、 :exc:`~.ext.commands.ExtensionNotFound` を発生させなくなりました。(:issue:`2244`, :issue:`2275`, :issue:`2291`)" -#: ../../whats_new.rst:854 +#: ../../whats_new.rst:887 msgid "|commands| Updating the :attr:`Paginator.suffix <.ext.commands.Paginator.suffix>` will not cause out of date calculations. (:issue:`2251`)" msgstr "|commands| :attr:`Paginator.suffix <.ext.commands.Paginator.suffix>` を更新しても、計算が古くならないようにしました。(:issue:`2251`)" -#: ../../whats_new.rst:855 +#: ../../whats_new.rst:888 msgid "|commands| Allow converters from custom extension packages. (:issue:`2369`, :issue:`2374`)" msgstr "|commands| カスタム拡張パッケージからのコンバータを許可します。(:issue:`2369`, :issue:`2374`) のようになります。" -#: ../../whats_new.rst:856 +#: ../../whats_new.rst:889 msgid "|commands| Fix issue with paginator prefix being ``None`` causing empty pages. (:issue:`2471`)" msgstr "|commands| paginator のプレフィックスが ``None`` であるために空のページが発生する問題を修正しました。(:issue:`2471`)" -#: ../../whats_new.rst:857 +#: ../../whats_new.rst:890 msgid "|commands| :class:`~.commands.Greedy` now ignores parsing errors rather than propagating them." msgstr "|commands| :class:`~.commands.Greedy` はパージングエラーを伝播するのではなく、無視するようになりました。" -#: ../../whats_new.rst:858 +#: ../../whats_new.rst:891 msgid "|commands| :meth:`Command.can_run <.ext.commands.Command.can_run>` now checks whether a command is disabled." msgstr "|commands| :meth:`Command.can_run <.ext.commands.Command.can_run>` がコマンドが無効かどうかをチェックするようになりました。" -#: ../../whats_new.rst:859 +#: ../../whats_new.rst:892 msgid "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` now takes into consideration nickname mentions. (:issue:`2489`)" msgstr "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` がニックネームのメンションを考慮するようになりました。 (:issue:`2489`)" -#: ../../whats_new.rst:860 +#: ../../whats_new.rst:893 msgid "|commands| :meth:`Context.send_help <.ext.commands.Context.send_help>` now properly propagates to the :meth:`HelpCommand.on_help_command_error <.ext.commands.HelpCommand.on_help_command_error>` handler." msgstr "|commands| :meth:`Context.send_help <.ext.commands.Context.send_help>` が :meth:`HelpCommand.on_help_command_error <.ext.commands.HelpCommand.on_help_command_error>` ハンドラに正しく伝播するようになりました。" -#: ../../whats_new.rst:865 +#: ../../whats_new.rst:898 msgid "The library now fully supports Python 3.8 without warnings." msgstr "ライブラリは警告なしに Python 3.8 を完全にサポートするようになりました。" -#: ../../whats_new.rst:866 +#: ../../whats_new.rst:899 msgid "Bump the dependency of ``websockets`` to 8.0 for those who can use it. (:issue:`2453`)" msgstr "依存ライブラリ ``websockets`` のバージョンを 8.0 に上げました。(:issue:`2453`)" -#: ../../whats_new.rst:867 +#: ../../whats_new.rst:900 msgid "Due to Discord providing :class:`Member` data in mentions, users will now be upgraded to :class:`Member` more often if mentioned." msgstr "Discordがメンションで :class:`Member` データを提供するようになったため、メンションされたユーザーが :class:`Member` により多くの機会でアップグレードされるようになりました。" -#: ../../whats_new.rst:868 +#: ../../whats_new.rst:901 msgid ":func:`utils.escape_markdown` now properly escapes new quote markdown." msgstr ":func:`utils.escape_markdown` が新しい引用マークダウンを正しくエスケープするようになりました。" -#: ../../whats_new.rst:869 +#: ../../whats_new.rst:902 msgid "The message cache can now be disabled by passing ``None`` to ``max_messages`` in :class:`Client`." msgstr "メッセージキャッシュを :class:`Client` の ``max_messages`` に ``None`` を渡すことで無効にできるようになりました。" -#: ../../whats_new.rst:870 +#: ../../whats_new.rst:903 msgid "The default message cache size has changed from 5000 to 1000 to accommodate small bots." msgstr "デフォルトのメッセージキャッシュサイズは、小さなボットに対応するために5000から1000に変更されました。" -#: ../../whats_new.rst:871 +#: ../../whats_new.rst:904 msgid "Lower memory usage by only creating certain objects as needed in :class:`Role`." msgstr ":class:`Role` にて、必要な場合のみ特定のオブジェクトを作成することによりメモリ使用量を削減しました。" -#: ../../whats_new.rst:872 +#: ../../whats_new.rst:905 msgid "There is now a sleep of 5 seconds before re-IDENTIFYing during a reconnect to prevent long loops of session invalidation." msgstr "セッションの無効化の長いループを防ぐために、再接続中に再度IDENTIFYする前に5秒間待つようになりました。" -#: ../../whats_new.rst:874 +#: ../../whats_new.rst:907 msgid "The rate limiting code now uses millisecond precision to have more granular rate limit handling." msgstr "レート制限コードは、より細かいレート制限処理を行うためにミリ秒の精度を使用するようになりました。" -#: ../../whats_new.rst:874 +#: ../../whats_new.rst:907 msgid "Along with that, the rate limiting code now uses Discord's response to wait. If you need to use the system clock again for whatever reason, consider passing ``assume_synced_clock`` in :class:`Client`." msgstr "それに伴い、レート制限コードはDiscordのレスポンスを使用して待つようになりました。 何らかの理由でシステムクロックを使用する必要がある場合は、 :class:`Client` で ``assume_synced_clock`` を渡すことを検討してください。" -#: ../../whats_new.rst:876 +#: ../../whats_new.rst:909 msgid "The performance of :attr:`Guild.default_role` has been improved from O(N) to O(1). (:issue:`2375`)" msgstr ":attr:`Guild.default_role` のパフォーマンスが O(N) から O(1) に改善されました。 (:issue:`2375`)" -#: ../../whats_new.rst:877 +#: ../../whats_new.rst:910 msgid "The performance of :attr:`Member.roles` has improved due to usage of caching to avoid surprising performance traps." msgstr "予期しないパフォーマンストラップを避けるために、キャッシュを使用して :attr:`Member.roles` のパフォーマンスを改善しました。" -#: ../../whats_new.rst:878 +#: ../../whats_new.rst:911 msgid "The GC is manually triggered during things that cause large deallocations (such as guild removal) to prevent memory fragmentation." msgstr "メモリの断片化を防ぐため、大規模なメモリの割り当て解除 (ギルドの除去など) が引き起こされた後に手動でガベージコレクションを行うようになりました。" -#: ../../whats_new.rst:879 +#: ../../whats_new.rst:912 msgid "There have been many changes to the documentation for fixes both for usability, correctness, and to fix some linter errors. Thanks to everyone who contributed to those." msgstr "ユーザビリティや正確性を向上させ、リンターエラーを修正するため、ドキュメントに多くの変更がありました。 貢献したすべての人に感謝します。" -#: ../../whats_new.rst:880 +#: ../../whats_new.rst:913 msgid "The loading of the opus module has been delayed which would make the result of :func:`opus.is_loaded` somewhat surprising." msgstr "opus モジュールの読み込みを遅延させるようにしました。このため :func:`opus.is_loaded` の結果が予想しないものになるかもしれません。" -#: ../../whats_new.rst:881 +#: ../../whats_new.rst:914 msgid "|commands| Usernames prefixed with @ inside DMs will properly convert using the :class:`User` converter. (:issue:`2498`)" msgstr "|commands| DM内の@で始まるユーザー名が、 :class:`User` コンバータを使用したとき正しく変換されるようになりました。 (:issue:`2498`)" -#: ../../whats_new.rst:882 +#: ../../whats_new.rst:915 msgid "|tasks| The task sleeping time will now take into consideration the amount of time the task body has taken before sleeping. (:issue:`2516`)" msgstr "|tasks| タスクの待ち時間が、タスク本体が実行するのにかかった時間を考慮に入れるようになりました。 (:issue:`2516`)" -#: ../../whats_new.rst:887 +#: ../../whats_new.rst:920 msgid "v1.2.5" msgstr "v1.2.5" -#: ../../whats_new.rst:892 +#: ../../whats_new.rst:925 msgid "Fix a bug that caused crashes due to missing ``animated`` field in Emoji structures in reactions." msgstr "絵文字構造の ``animated`` フィールドが存在しないとしてクラッシュするバグを修正しました。" -#: ../../whats_new.rst:897 +#: ../../whats_new.rst:930 msgid "v1.2.4" msgstr "v1.2.4" -#: ../../whats_new.rst:902 +#: ../../whats_new.rst:935 msgid "Fix a regression when :attr:`Message.channel` would be ``None``." msgstr ":attr:`Message.channel` が ``None`` になるリグレッションを修正しました。" -#: ../../whats_new.rst:903 +#: ../../whats_new.rst:936 msgid "Fix a regression where :attr:`Message.edited_at` would not update during edits." msgstr ":attr:`Message.edited_at` が編集中に更新されないリグレッションを修正しました。" -#: ../../whats_new.rst:904 +#: ../../whats_new.rst:937 msgid "Fix a crash that would trigger during message updates (:issue:`2265`, :issue:`2287`)." msgstr "メッセージの更新中に引き起こされるクラッシュを修正しました。(:issue:`2265`, :issue:`2287`)" -#: ../../whats_new.rst:905 +#: ../../whats_new.rst:938 msgid "Fix a bug when :meth:`VoiceChannel.connect` would not return (:issue:`2274`, :issue:`2372`, :issue:`2373`, :issue:`2377`)." msgstr ":meth:`VoiceChannel.connect` が応答しないバグを修正しました。(:issue:`2274`、 :issue:`2372`、 :issue:`2373`、 :issue:`2377`)" -#: ../../whats_new.rst:906 +#: ../../whats_new.rst:939 msgid "Fix a crash relating to token-less webhooks (:issue:`2364`)." msgstr "トークンのないWebhookに関するクラッシュを修正しました。(:issue:`2364`)" -#: ../../whats_new.rst:907 +#: ../../whats_new.rst:940 msgid "Fix issue where :attr:`Guild.premium_subscription_count` would be ``None`` due to a Discord bug. (:issue:`2331`, :issue:`2376`)." msgstr "Discord バグにより :attr:`Guild.premium_subscription_count` が ``None`` になる問題を修正しました。(:issue:`2331`, :issue:`2376`)" -#: ../../whats_new.rst:912 +#: ../../whats_new.rst:945 msgid "v1.2.3" msgstr "v1.2.3" -#: ../../whats_new.rst:917 +#: ../../whats_new.rst:950 msgid "Fix an AttributeError when accessing :attr:`Member.premium_since` in :func:`on_member_update`. (:issue:`2213`)" msgstr ":func:`on_member_update` で :attr:`Member.premium_since` にアクセスした際の AttributeError を修正しました。 (:issue:`2213`)" -#: ../../whats_new.rst:918 +#: ../../whats_new.rst:951 msgid "Handle :exc:`asyncio.CancelledError` in :meth:`abc.Messageable.typing` context manager. (:issue:`2218`)" msgstr ":meth:`abc.Messageable.typing` コンテキストマネージャでの :exc:`asyncio.CanceledError` を処理するようにしました。 (:issue:`2218`)" -#: ../../whats_new.rst:919 +#: ../../whats_new.rst:952 msgid "Raise the max encoder bitrate to 512kbps to account for nitro boosting. (:issue:`2232`)" msgstr "ニトロブーストを考慮し、最大エンコーダビットレートを512kbpsに引き上げ。 (:issue:`2232`)" -#: ../../whats_new.rst:920 +#: ../../whats_new.rst:953 msgid "Properly propagate exceptions in :meth:`Client.run`. (:issue:`2237`)" msgstr ":meth:`Client.run` にて例外を適切に伝播するようにしました。(:issue:`2237`)" -#: ../../whats_new.rst:921 +#: ../../whats_new.rst:954 msgid "|commands| Ensure cooldowns are properly copied when used in cog level ``command_attrs``." msgstr "|commands| コグレベル ``command_attrs`` で使用されるクールダウンが正しくコピーされるようにしました。" -#: ../../whats_new.rst:926 +#: ../../whats_new.rst:959 msgid "v1.2.2" msgstr "v1.2.2" -#: ../../whats_new.rst:931 +#: ../../whats_new.rst:964 msgid "Audit log related attribute access have been fixed to not error out when they shouldn't have." msgstr "監査ログ関連の属性アクセスは、本来すべきでないときにエラーを起こさないよう修正されました。" -#: ../../whats_new.rst:936 +#: ../../whats_new.rst:969 msgid "v1.2.1" msgstr "v1.2.1" -#: ../../whats_new.rst:941 +#: ../../whats_new.rst:974 msgid ":attr:`User.avatar_url` and related attributes no longer raise an error." msgstr ":attr:`User.avatar_url` と関連する属性がエラーを引き起こさないように修正しました。" -#: ../../whats_new.rst:942 +#: ../../whats_new.rst:975 msgid "More compatibility shims with the ``enum.Enum`` code." msgstr "``enum.Enum`` コードの互換性が向上しました。" -#: ../../whats_new.rst:947 +#: ../../whats_new.rst:980 msgid "v1.2.0" msgstr "v1.2.0" -#: ../../whats_new.rst:949 +#: ../../whats_new.rst:982 msgid "This update mainly brings performance improvements and various nitro boosting attributes (referred to in the API as \"premium guilds\")." msgstr "今回のアップデートでは、主にパフォーマンスの向上と、さまざまなニトロブースト属性(APIでは「プレミアムギルド」と呼ばれます) が追加されました。" -#: ../../whats_new.rst:954 +#: ../../whats_new.rst:987 msgid "Add :attr:`Guild.premium_tier` to query the guild's current nitro boost level." msgstr ":attr:`Guild.premium_tier` で、ギルドの現在のニトロブーストレベルが取得できます。" -#: ../../whats_new.rst:955 +#: ../../whats_new.rst:988 msgid "Add :attr:`Guild.emoji_limit`, :attr:`Guild.bitrate_limit`, :attr:`Guild.filesize_limit` to query the new limits of a guild when taking into consideration boosting." msgstr "ブーストを考慮してギルドの新しい制限を取得する :attr:`Guild.emoji_limit` 、 :attr:`Guild.bitrate_limit` 、 :attr:`Guild.filesize_limit` を追加しました。" -#: ../../whats_new.rst:956 +#: ../../whats_new.rst:989 msgid "Add :attr:`Guild.premium_subscription_count` to query how many members are boosting a guild." msgstr "ギルドをブーストしているメンバー数を取得する :attr:`Guild.premium_subscription_count` を追加しました。" -#: ../../whats_new.rst:957 +#: ../../whats_new.rst:990 msgid "Add :attr:`Member.premium_since` to query since when a member has boosted a guild." msgstr "メンバーがギルドをブーストし始めた日時を取得する :attr:`Member.premium_since` を追加しました。" -#: ../../whats_new.rst:958 +#: ../../whats_new.rst:991 msgid "Add :attr:`Guild.premium_subscribers` to query all the members currently boosting the guild." msgstr "現在ギルドをブーストしているメンバーをすべて取得する :attr:`Guild.premium_subscribers` を追加しました。" -#: ../../whats_new.rst:959 +#: ../../whats_new.rst:992 msgid "Add :attr:`Guild.system_channel_flags` to query the settings for a guild's :attr:`Guild.system_channel`." msgstr "ギルドの :attr:`Guild.system_channel` の設定を取得する :attr:`Guild.system_channel_flags` を追加しました。" -#: ../../whats_new.rst:960 +#: ../../whats_new.rst:993 msgid "This includes a new type named :class:`SystemChannelFlags`" msgstr ":class:`SystemChannelFlags` という新しい型も含まれます。" -#: ../../whats_new.rst:961 +#: ../../whats_new.rst:994 msgid "Add :attr:`Emoji.available` to query if an emoji can be used (within the guild or otherwise)." msgstr "絵文字が(ギルド内またはそれ以外で)利用できるかを確認する :attr:`Emoji.available` を追加しました。" -#: ../../whats_new.rst:962 +#: ../../whats_new.rst:995 msgid "Add support for animated icons in :meth:`Guild.icon_url_as` and :attr:`Guild.icon_url`." msgstr ":meth:`Guild.icon_url_as` と :attr:`Guild.icon_url` にアニメーションアイコンのサポートを追加しました。" -#: ../../whats_new.rst:963 +#: ../../whats_new.rst:996 msgid "Add :meth:`Guild.is_icon_animated`." msgstr ":meth:`Guild.is_icon_animated` を追加しました。" -#: ../../whats_new.rst:964 +#: ../../whats_new.rst:997 msgid "Add support for the various new :class:`MessageType` involving nitro boosting." msgstr "ニトロブーストに関する様々な新しい :class:`MessageType` のサポートを追加しました。" -#: ../../whats_new.rst:965 +#: ../../whats_new.rst:998 msgid "Add :attr:`VoiceRegion.india`. (:issue:`2145`)" msgstr ":attr:`VoiceRegion.india` を追加しました。 (:issue:`2145`)" -#: ../../whats_new.rst:966 +#: ../../whats_new.rst:999 msgid "Add :meth:`Embed.insert_field_at`. (:issue:`2178`)" msgstr ":meth:`Embed.insert_field_at` を追加しました。 (:issue:`2178`)" -#: ../../whats_new.rst:967 +#: ../../whats_new.rst:1000 msgid "Add a ``type`` attribute for all channels to their appropriate :class:`ChannelType`. (:issue:`2185`)" msgstr "すべてのチャンネルに対し、適切な :class:`ChannelType` を返す ``type`` 属性を追加しました。 (:issue:`2185` )" -#: ../../whats_new.rst:968 +#: ../../whats_new.rst:1001 msgid "Add :meth:`Client.fetch_channel` to fetch a channel by ID via HTTP. (:issue:`2169`)" msgstr "HTTP経由でチャンネルをIDにより取得する、 :meth:`Client.fetch_channel` を追加しました。(:issue:`2169`)" -#: ../../whats_new.rst:969 +#: ../../whats_new.rst:1002 msgid "Add :meth:`Guild.fetch_channels` to fetch all channels via HTTP. (:issue:`2169`)" msgstr "HTTP経由でチャンネルをすべて取得する、 :meth:`Guild.fetch_channels` を追加しました。(:issue:`2169`)" -#: ../../whats_new.rst:970 +#: ../../whats_new.rst:1003 msgid "|tasks| Add :meth:`Loop.stop <.ext.tasks.Loop.stop>` to gracefully stop a task rather than cancelling." msgstr "|tasks| タスクをキャンセルするのではなく、現在のタスクが終了後に停止させる :meth:`Loop.stop <.ext.tasks.Loop.stop>` を追加しました。" -#: ../../whats_new.rst:971 +#: ../../whats_new.rst:1004 msgid "|tasks| Add :meth:`Loop.failed <.ext.tasks.Loop.failed>` to query if a task had failed somehow." msgstr "|tasks| タスクが何らかの理由で失敗したかを調べる :meth:`Loop.failed <.ext.tasks.Loop.failed>` を追加しました。" -#: ../../whats_new.rst:972 +#: ../../whats_new.rst:1005 msgid "|tasks| Add :meth:`Loop.change_interval <.ext.tasks.Loop.change_interval>` to change the sleep interval at runtime (:issue:`2158`, :issue:`2162`)" msgstr "|tasks| 実行時に待機時間を変更できる :meth:`Loop.change_interval <.ext.tasks.Loop.change_interval>` を追加しました。(:issue:`2158`, :issue:`2162`)" -#: ../../whats_new.rst:977 +#: ../../whats_new.rst:1010 msgid "Fix internal error when using :meth:`Guild.prune_members`." msgstr ":meth:`Guild.prune_members` を使用した場合の内部エラーを修正しました。" -#: ../../whats_new.rst:978 +#: ../../whats_new.rst:1011 msgid "|commands| Fix :attr:`.Command.invoked_subcommand` being invalid in many cases." msgstr "|commands| 多くの場合において :attr:`.Command.invoked_subcommand` が誤っているのを修正しました。" -#: ../../whats_new.rst:979 +#: ../../whats_new.rst:1012 msgid "|tasks| Reset iteration count when the loop terminates and is restarted." msgstr "|tasks| ループが終了し、再起動されたときに反復回数をリセットするようにしました。" -#: ../../whats_new.rst:980 +#: ../../whats_new.rst:1013 msgid "|tasks| The decorator interface now works as expected when stacking (:issue:`2154`)" msgstr "|tasks| デコレータインターフェースをスタックした時に期待通り動作するようになりました。 (:issue:`2154`)" -#: ../../whats_new.rst:986 +#: ../../whats_new.rst:1019 msgid "Improve performance of all Enum related code significantly." msgstr "列挙型に関連するすべてのコードのパフォーマンスを大幅に向上させました。" -#: ../../whats_new.rst:986 +#: ../../whats_new.rst:1019 msgid "This was done by replacing the ``enum.Enum`` code with an API compatible one." msgstr "これは、 ``enum.Enum`` コードを API 互換のコードに置き換えることによって行われました。" -#: ../../whats_new.rst:987 +#: ../../whats_new.rst:1020 msgid "This should not be a breaking change for most users due to duck-typing." msgstr "ダックタイピングを使用しているため、ほとんどのユーザーにとっては破壊的変更ではありません。" -#: ../../whats_new.rst:988 +#: ../../whats_new.rst:1021 msgid "Improve performance of message creation by about 1.5x." msgstr "メッセージ作成のパフォーマンスを約1.5倍向上させました。" -#: ../../whats_new.rst:989 +#: ../../whats_new.rst:1022 msgid "Improve performance of message editing by about 1.5-4x depending on payload size." msgstr "メッセージ編集のパフォーマンスが約1.5~4倍向上しました。(内容のサイズに依存します)" -#: ../../whats_new.rst:990 +#: ../../whats_new.rst:1023 msgid "Improve performance of attribute access on :class:`Member` about by 2x." msgstr ":class:`Member` の属性へのアクセスのパフォーマンスが2倍向上しました。" -#: ../../whats_new.rst:991 +#: ../../whats_new.rst:1024 msgid "Improve performance of :func:`utils.get` by around 4-6x depending on usage." msgstr ":func:`utils.get` のパフォーマンスを、使用状況に応じて約 4-6倍 向上させました。" -#: ../../whats_new.rst:992 +#: ../../whats_new.rst:1025 msgid "Improve performance of event parsing lookup by around 2.5x." msgstr "イベント解析中のルックアップのパフォーマンスを約2.5倍向上させました。" -#: ../../whats_new.rst:993 +#: ../../whats_new.rst:1026 msgid "Keyword arguments in :meth:`Client.start` and :meth:`Client.run` are now validated (:issue:`953`, :issue:`2170`)" msgstr ":meth:`Client.start` と :meth:`Client.run` のキーワード引数を検証するようにしました。 (:issue:`953`, :issue:`2170`)" -#: ../../whats_new.rst:994 +#: ../../whats_new.rst:1027 msgid "The Discord error code is now shown in the exception message for :exc:`HTTPException`." msgstr ":exc:`HTTPException` の例外メッセージにDiscordのエラーコードが表示されるようになりました。" -#: ../../whats_new.rst:995 +#: ../../whats_new.rst:1028 msgid "Internal tasks launched by the library will now have their own custom ``__repr__``." msgstr "ライブラリによって実行された内部タスクに独自のカスタム ``__repr__`` を追加しました。" -#: ../../whats_new.rst:996 +#: ../../whats_new.rst:1029 msgid "All public facing types should now have a proper and more detailed ``__repr__``." msgstr "すべての公開された型に、適切でより詳細な ``__repr__`` を追加しました。" -#: ../../whats_new.rst:997 +#: ../../whats_new.rst:1030 msgid "|tasks| Errors are now logged via the standard :mod:`py:logging` module." msgstr "|tasks| 標準の :mod:`py:logging` モジュールを介してエラーが記録されるようになりました。" -#: ../../whats_new.rst:1002 +#: ../../whats_new.rst:1035 msgid "v1.1.1" msgstr "v1.1.1" -#: ../../whats_new.rst:1007 +#: ../../whats_new.rst:1040 msgid "Webhooks do not overwrite data on retrying their HTTP requests (:issue:`2140`)" msgstr "WebhookがHTTPリクエストを再試行する時にデータを上書きしないようにしました。 (:issue:`2140`)" -#: ../../whats_new.rst:1012 +#: ../../whats_new.rst:1045 msgid "Add back signal handling to :meth:`Client.run` due to issues some users had with proper cleanup." msgstr "一部のユーザーが適切なクリーンアップを行うときに問題が生じていたため、 :meth:`Client.run` にシグナル処理を再度追加しました。" -#: ../../whats_new.rst:1017 +#: ../../whats_new.rst:1050 msgid "v1.1.0" msgstr "v1.1.0" -#: ../../whats_new.rst:1022 +#: ../../whats_new.rst:1055 msgid "**There is a new extension dedicated to making background tasks easier.**" msgstr "**バックグラウンドタスクを簡単にするための新しい拡張機能が追加されました。**" -#: ../../whats_new.rst:1023 +#: ../../whats_new.rst:1056 msgid "You can check the documentation here: :ref:`ext_tasks_api`." msgstr "使い方の説明は、 :ref:`ext_tasks_api` で確認できます。" -#: ../../whats_new.rst:1024 +#: ../../whats_new.rst:1057 msgid "Add :attr:`Permissions.stream` permission. (:issue:`2077`)" msgstr ":attr:`Permissions.stream` 権限を追加しました。 (:issue:`2077`)" -#: ../../whats_new.rst:1025 +#: ../../whats_new.rst:1058 msgid "Add equality comparison and hash support to :class:`Asset`" msgstr ":class:`Asset` に等価比較とハッシュサポートを追加しました。" -#: ../../whats_new.rst:1026 +#: ../../whats_new.rst:1059 msgid "Add ``compute_prune_members`` parameter to :meth:`Guild.prune_members` (:issue:`2085`)" msgstr ":meth:`Guild.prune_members` に ``compute_prune_members`` パラメータを追加しました。 (:issue:`2085`)" -#: ../../whats_new.rst:1027 +#: ../../whats_new.rst:1060 msgid "Add :attr:`Client.cached_messages` attribute to fetch the message cache (:issue:`2086`)" msgstr "メッセージキャッシュを取得する :attr:`Client.cached_messages` 属性を追加しました。 (:issue:`2086`)" -#: ../../whats_new.rst:1028 +#: ../../whats_new.rst:1061 msgid "Add :meth:`abc.GuildChannel.clone` to clone a guild channel. (:issue:`2093`)" msgstr "ギルドのチャンネルをコピーする :meth:`abc.GuildChannel.clone` メソッドが追加されました。( :issue:`2093` )" -#: ../../whats_new.rst:1029 +#: ../../whats_new.rst:1062 msgid "Add ``delay`` keyword-only argument to :meth:`Message.delete` (:issue:`2094`)" msgstr ":meth:`Message.delete` にキーワード限定引数 ``delay`` が追加されました。( :issue:`2094` )" -#: ../../whats_new.rst:1030 +#: ../../whats_new.rst:1063 msgid "Add support for ``<:name:id>`` when adding reactions (:issue:`2095`)" msgstr "``<:name:id>`` のフォーマットでリアクションを追加できるようになりました。( :issue:`2095` )" -#: ../../whats_new.rst:1031 +#: ../../whats_new.rst:1064 msgid "Add :meth:`Asset.read` to fetch the bytes content of an asset (:issue:`2107`)" msgstr "アセットを ``bytes`` オブジェクトとして取得する :meth:`Asset.read` メソッドが追加されました( :issue:`2107` )" -#: ../../whats_new.rst:1032 +#: ../../whats_new.rst:1065 msgid "Add :meth:`Attachment.read` to fetch the bytes content of an attachment (:issue:`2118`)" msgstr "添付ファイルを ``bytes`` オブジェクトとして取得する :meth:`Attachment.read` メソッドが追加されました( :issue:`2118` )" -#: ../../whats_new.rst:1033 +#: ../../whats_new.rst:1066 msgid "Add support for voice kicking by passing ``None`` to :meth:`Member.move_to`." msgstr ":meth:`Member.move_to` に ``None`` を渡すことでボイスチャンネルから強制切断できるようになりました。" -#: ../../whats_new.rst:1036 -#: ../../whats_new.rst:1057 -#: ../../whats_new.rst:1076 +#: ../../whats_new.rst:1069 +#: ../../whats_new.rst:1090 +#: ../../whats_new.rst:1109 msgid "``discord.ext.commands``" msgstr "``discord.ext.commands``" -#: ../../whats_new.rst:1038 +#: ../../whats_new.rst:1071 msgid "Add new :func:`~.commands.dm_only` check." msgstr ":func:`~.commands.dm_only` チェックが追加されました。" -#: ../../whats_new.rst:1039 +#: ../../whats_new.rst:1072 msgid "Support callable converters in :data:`~.commands.Greedy`" msgstr "呼び出し可能オブジェクトのコンバーターを :data:`~.commands.Greedy` で使えるようになりました。" -#: ../../whats_new.rst:1040 +#: ../../whats_new.rst:1073 msgid "Add new :class:`~.commands.MessageConverter`." msgstr ":class:`~.commands.MessageConverter` が追加されました。" -#: ../../whats_new.rst:1041 +#: ../../whats_new.rst:1074 msgid "This allows you to use :class:`Message` as a type hint in functions." msgstr "これにより、 :class:`Message` を関数の型ヒントで使えるようになりました。" -#: ../../whats_new.rst:1042 +#: ../../whats_new.rst:1075 msgid "Allow passing ``cls`` in the :func:`~.commands.group` decorator (:issue:`2061`)" msgstr ":func:`~.commands.group` に ``cls`` を渡せるようになりました( :issue:`2061` )" -#: ../../whats_new.rst:1043 +#: ../../whats_new.rst:1076 msgid "Add :attr:`.Command.parents` to fetch the parents of a command (:issue:`2104`)" msgstr "親コマンドを取得する :attr:`.Command.parents` が追加されました。( :issue:`2104` )" -#: ../../whats_new.rst:1049 +#: ../../whats_new.rst:1082 msgid "Fix :exc:`AttributeError` when using ``__repr__`` on :class:`Widget`." msgstr ":class:`Widget` の ``__repr__`` で :exc:`AttributeError` が発生するバグを修正しました。" -#: ../../whats_new.rst:1050 +#: ../../whats_new.rst:1083 msgid "Fix issue with :attr:`abc.GuildChannel.overwrites` returning ``None`` for keys." msgstr ":attr:`abc.GuildChannel.overwrites` のキーが ``None`` になるバグを修正しました。" -#: ../../whats_new.rst:1051 +#: ../../whats_new.rst:1084 msgid "Remove incorrect legacy NSFW checks in e.g. :meth:`TextChannel.is_nsfw`." msgstr ":meth:`TextChannel.is_nsfw` 等でのNSFWのチェックを修正しました。" -#: ../../whats_new.rst:1052 +#: ../../whats_new.rst:1085 msgid "Fix :exc:`UnboundLocalError` when :class:`RequestsWebhookAdapter` raises an error." msgstr ":class:`RequestsWebhookAdapter` でエラーが発生したときの :exc:`UnboundLocalError` を修正しました。" -#: ../../whats_new.rst:1053 +#: ../../whats_new.rst:1086 msgid "Fix bug where updating your own user did not update your member instances." msgstr "ボットのユーザーをアップデートしてもメンバーオブジェクトが更新されないバグを修正しました。" -#: ../../whats_new.rst:1054 +#: ../../whats_new.rst:1087 msgid "Tighten constraints of ``__eq__`` in :class:`Spotify` objects (:issue:`2113`, :issue:`2117`)" msgstr ":class:`Spotify` の ``__eq__`` の条件を厳しくしました。( :issue:`2113`, :issue:`2117` )" -#: ../../whats_new.rst:1059 +#: ../../whats_new.rst:1092 msgid "Fix lambda converters in a non-module context (e.g. ``eval``)." msgstr "モジュール以外での無名コンバーターを修正しました。(例: ``eval`` )" -#: ../../whats_new.rst:1060 +#: ../../whats_new.rst:1093 msgid "Use message creation time for reference time when computing cooldowns." msgstr "クールダウンの計算にメッセージの作成時間を使用するようになりました。" -#: ../../whats_new.rst:1061 +#: ../../whats_new.rst:1094 msgid "This prevents cooldowns from triggering during e.g. a RESUME session." msgstr "これにより、RESUME中でのクールダウンの挙動が修正されました。" -#: ../../whats_new.rst:1062 +#: ../../whats_new.rst:1095 msgid "Fix the default :func:`on_command_error` to work with new-style cogs (:issue:`2094`)" msgstr "新しいスタイルのコグのため、 :func:`on_command_error` のデフォルトの挙動を修正しました。( :issue:`2094` )" -#: ../../whats_new.rst:1063 +#: ../../whats_new.rst:1096 msgid "DM channels are now recognised as NSFW in :func:`~.commands.is_nsfw` check." msgstr "DMチャンネルが :func:`~.commands.is_nsfw` に認識されるようになりました。" -#: ../../whats_new.rst:1064 +#: ../../whats_new.rst:1097 msgid "Fix race condition with help commands (:issue:`2123`)" msgstr "ヘルプコマンドの競合状態を修正しました。 (:issue:`2123`)" -#: ../../whats_new.rst:1065 +#: ../../whats_new.rst:1098 msgid "Fix cog descriptions not showing in :class:`~.commands.MinimalHelpCommand` (:issue:`2139`)" msgstr ":class:`~.commands.MinimalHelpCommand` にコグの説明が表示されるようになりました。( :issue:`2139` )" -#: ../../whats_new.rst:1070 +#: ../../whats_new.rst:1103 msgid "Improve the performance of internal enum creation in the library by about 5x." msgstr "ライブラリ内での列挙型の作成が約5倍早くなりました。" -#: ../../whats_new.rst:1071 +#: ../../whats_new.rst:1104 msgid "Make the output of ``python -m discord --version`` a bit more useful." msgstr "``python -m discord --version`` の出力を改善しました。" -#: ../../whats_new.rst:1072 +#: ../../whats_new.rst:1105 msgid "The loop cleanup facility has been rewritten again." msgstr "ループのクリーンアップがまた書き直されました。" -#: ../../whats_new.rst:1073 +#: ../../whats_new.rst:1106 msgid "The signal handling in :meth:`Client.run` has been removed." msgstr ":meth:`Client.run` でのシグナル制御が削除されました。" -#: ../../whats_new.rst:1078 +#: ../../whats_new.rst:1111 msgid "Custom exception classes are now used for all default checks in the library (:issue:`2101`)" msgstr "ライブラリ内の全てのチェックがカスタム例外クラスを使うようになりました( :issue:`2101` )" -#: ../../whats_new.rst:1084 +#: ../../whats_new.rst:1117 msgid "v1.0.1" msgstr "v1.0.1" -#: ../../whats_new.rst:1089 +#: ../../whats_new.rst:1122 msgid "Fix issue with speaking state being cast to ``int`` when it was invalid." msgstr "スピーキング状態が無効なときに ``int`` にキャストした場合に発生する問題を修正しました。" -#: ../../whats_new.rst:1090 +#: ../../whats_new.rst:1123 msgid "Fix some issues with loop cleanup that some users experienced on Linux machines." msgstr "一部のユーザーがLinuxマシンで遭遇したループクリーンアップに関する問題を修正しました。" -#: ../../whats_new.rst:1091 +#: ../../whats_new.rst:1124 msgid "Fix voice handshake race condition (:issue:`2056`, :issue:`2063`)" msgstr "ボイスハンドシェイクの競合状態を修正しました。 (:issue:`2056`, :issue:`2063`)" -#: ../../whats_new.rst:1096 +#: ../../whats_new.rst:1129 msgid "v1.0.0" msgstr "v1.0.0" -#: ../../whats_new.rst:1098 +#: ../../whats_new.rst:1131 msgid "The changeset for this version are too big to be listed here, for more information please see :ref:`the migrating page `." msgstr "このバージョンの変更は大きすぎるため、この場所に収まりきりません。詳細については :ref:`移行についてのページ ` を参照してください。" -#: ../../whats_new.rst:1105 +#: ../../whats_new.rst:1138 msgid "v0.16.6" msgstr "v0.16.6" -#: ../../whats_new.rst:1110 +#: ../../whats_new.rst:1143 msgid "Fix issue with :meth:`Client.create_server` that made it stop working." msgstr ":meth:`Client.create_server` によって動作が停止する問題を修正しました。" -#: ../../whats_new.rst:1111 +#: ../../whats_new.rst:1144 msgid "Fix main thread being blocked upon calling ``StreamPlayer.stop``." msgstr "``StreamPlayer.stop`` の呼び出し時にメインスレッドがブロックされるのを修正しました。" -#: ../../whats_new.rst:1112 +#: ../../whats_new.rst:1145 msgid "Handle HEARTBEAT_ACK and resume gracefully when it occurs." msgstr "HEARTBEAT_ACKを処理し、正常に再開します。" -#: ../../whats_new.rst:1113 +#: ../../whats_new.rst:1146 msgid "Fix race condition when pre-emptively rate limiting that caused releasing an already released lock." msgstr "既に開放されているロックを解放しようとする原因になっていた先制的なレート制限を行っている時の競合状態を修正しました。" -#: ../../whats_new.rst:1114 +#: ../../whats_new.rst:1147 msgid "Fix invalid state errors when immediately cancelling a coroutine." msgstr "コルーチンを直ちにキャンセルするときに無効な状態になるエラーを修正しました。" -#: ../../whats_new.rst:1119 +#: ../../whats_new.rst:1152 msgid "v0.16.1" msgstr "v0.16.1" -#: ../../whats_new.rst:1121 +#: ../../whats_new.rst:1154 msgid "This release is just a bug fix release with some better rate limit implementation." msgstr "このリリースはバグ修正であり、いくつかのレート制限の実装が改善されています。" -#: ../../whats_new.rst:1126 +#: ../../whats_new.rst:1159 msgid "Servers are now properly chunked for user bots." msgstr "ユーザーボットがサーバーを適切にチャンクするようにしました。" -#: ../../whats_new.rst:1127 +#: ../../whats_new.rst:1160 msgid "The CDN URL is now used instead of the API URL for assets." msgstr "アセットのAPI URLの代わりにCDN URLが使用されるようになりました。" -#: ../../whats_new.rst:1128 +#: ../../whats_new.rst:1161 msgid "Rate limit implementation now tries to use header information if possible." msgstr "レート制限の実装が可能な場合ヘッダ情報を利用するようにしました。" -#: ../../whats_new.rst:1129 +#: ../../whats_new.rst:1162 msgid "Event loop is now properly propagated (:issue:`420`)" msgstr "イベントループが正しく伝播するようにしました。 (:issue:`420`)" -#: ../../whats_new.rst:1130 +#: ../../whats_new.rst:1163 msgid "Allow falsey values in :meth:`Client.send_message` and :meth:`Client.send_file`." msgstr ":meth:`Client.send_message` と :meth:`Client.send_file` でFalseに変換される値を利用できるようにしました。" -#: ../../whats_new.rst:1135 +#: ../../whats_new.rst:1168 msgid "v0.16.0" msgstr "v0.16.0" -#: ../../whats_new.rst:1140 +#: ../../whats_new.rst:1173 msgid "Add :attr:`Channel.overwrites` to get all the permission overwrites of a channel." msgstr "チャンネルの権限上書きをすべて取得する :attr:`Channel.overwrites` を追加しました。" -#: ../../whats_new.rst:1141 +#: ../../whats_new.rst:1174 msgid "Add :attr:`Server.features` to get information about partnered servers." msgstr "パートナーサーバーの情報を得ることのできる :attr:`Server.features` を追加しました。" -#: ../../whats_new.rst:1146 +#: ../../whats_new.rst:1179 msgid "Timeout when waiting for offline members while triggering :func:`on_ready`." msgstr ":func:`on_ready` を実行中にオフラインメンバーを待っているとき、タイムアウトするようにしました。" -#: ../../whats_new.rst:1148 +#: ../../whats_new.rst:1181 msgid "The fact that we did not timeout caused a gigantic memory leak in the library that caused thousands of duplicate :class:`Member` instances causing big memory spikes." msgstr "以前はタイムアウトしなかったため、ライブラリで数千もの :class:`Member` インスタンスが作成されメモリ使用量が大幅に上昇する大規模なメモリリークが発生していました。" -#: ../../whats_new.rst:1151 +#: ../../whats_new.rst:1184 msgid "Discard null sequences in the gateway." msgstr "ゲートウェイでヌル値のシーケンスを破棄するようにしました。" -#: ../../whats_new.rst:1153 +#: ../../whats_new.rst:1186 msgid "The fact these were not discarded meant that :func:`on_ready` kept being called instead of :func:`on_resumed`. Since this has been corrected, in most cases :func:`on_ready` will be called once or twice with :func:`on_resumed` being called much more often." msgstr "以前は破棄されていなかったため、 :func:`on_ready` が :func:`on_resumed` の代わりに呼び出されることがありました。これが修正されたため、多くの場合では :func:`on_ready` は一、二回呼び出されるだけで、 :func:`on_resumed` がより頻繁に呼び出されるようになります。" -#: ../../whats_new.rst:1160 +#: ../../whats_new.rst:1193 msgid "v0.15.1" msgstr "v0.15.1" -#: ../../whats_new.rst:1162 +#: ../../whats_new.rst:1195 msgid "Fix crash on duplicate or out of order reactions." msgstr "重複したり、順番になっていないリアクションによるクラッシュを修正しました。" -#: ../../whats_new.rst:1167 +#: ../../whats_new.rst:1200 msgid "v0.15.0" msgstr "v0.15.0" -#: ../../whats_new.rst:1172 +#: ../../whats_new.rst:1205 msgid "Rich Embeds for messages are now supported." msgstr "メッセージのリッチな埋め込みをサポートするようにしました。" -#: ../../whats_new.rst:1174 +#: ../../whats_new.rst:1207 msgid "To do so, create your own :class:`Embed` and pass the instance to the ``embed`` keyword argument to :meth:`Client.send_message` or :meth:`Client.edit_message`." msgstr "このためには、自分の :class:`Embed` を作成してインスタンスを :meth:`Client.send_message` や :meth:`Client.edit_message` の ``embed`` キーワード引数に渡してください。" -#: ../../whats_new.rst:1175 +#: ../../whats_new.rst:1208 msgid "Add :meth:`Client.clear_reactions` to remove all reactions from a message." msgstr "メッセージからすべてリアクションを除去する :meth:`Client.clear_reactions` を追加しました。" -#: ../../whats_new.rst:1176 +#: ../../whats_new.rst:1209 msgid "Add support for MESSAGE_REACTION_REMOVE_ALL event, under :func:`on_reaction_clear`." msgstr ":func:`on_reaction_clear` の下にMESSAGE_REMOVE_ALL イベントのサポートを追加しました。" -#: ../../whats_new.rst:1177 +#: ../../whats_new.rst:1210 msgid "Add :meth:`Permissions.update` and :meth:`PermissionOverwrite.update` for bulk permission updates." msgstr "一括して権限を更新する、 :meth:`Permissions.update` と :meth:`PermissionOverwrite.update` を追加しました。" -#: ../../whats_new.rst:1179 +#: ../../whats_new.rst:1212 msgid "This allows you to use e.g. ``p.update(read_messages=True, send_messages=False)`` in a single line." msgstr "これにより、例えば ``p.update(read_messages=True, send_messages=False)`` のように一行で使用できます。" -#: ../../whats_new.rst:1180 +#: ../../whats_new.rst:1213 msgid "Add :meth:`PermissionOverwrite.is_empty` to check if the overwrite is empty (i.e. has no overwrites set explicitly as true or false)." msgstr "権限上書きが空か(すなわち、明示的にtrueまたはfalseに設定されている上書きが存在しないか)を確認する :meth:`PermissionOverwrite.is_empty` を追加しました。" -#: ../../whats_new.rst:1182 +#: ../../whats_new.rst:1215 msgid "For the command extension, the following changed:" msgstr "コマンド拡張の場合、以下のことが変更されます。" -#: ../../whats_new.rst:1184 +#: ../../whats_new.rst:1217 msgid "``Context`` is no longer slotted to facilitate setting dynamic attributes." msgstr "``Context`` への動的属性の設定を容易にするためにスロット制限を除去しました。" -#: ../../whats_new.rst:1189 +#: ../../whats_new.rst:1222 msgid "v0.14.3" msgstr "v0.14.3" -#: ../../whats_new.rst:1194 +#: ../../whats_new.rst:1227 msgid "Fix crash when dealing with MESSAGE_REACTION_REMOVE" msgstr "MESSAGE_REACTION_REMOVEを扱う際のクラッシュを修正しました" -#: ../../whats_new.rst:1195 +#: ../../whats_new.rst:1228 msgid "Fix incorrect buckets for reactions." msgstr "リアクションに誤ったバケットが適用されていたのを修正しました。" -#: ../../whats_new.rst:1200 +#: ../../whats_new.rst:1233 msgid "v0.14.2" msgstr "v0.14.2" -#: ../../whats_new.rst:1206 +#: ../../whats_new.rst:1239 msgid ":meth:`Client.wait_for_reaction` now returns a namedtuple with ``reaction`` and ``user`` attributes." msgstr ":meth:`Client.wait_for_reaction` が ``reaction`` と ``user`` 属性を持つ名前付きタプルを返すようになりました。" -#: ../../whats_new.rst:1206 +#: ../../whats_new.rst:1239 msgid "This is for better support in the case that ``None`` is returned since tuple unpacking can lead to issues." msgstr "これは、タプルを展開すると問題につながる可能性がある、 ``None`` が返された場合のより良いサポートのためです。" -#: ../../whats_new.rst:1211 +#: ../../whats_new.rst:1244 msgid "Fix bug that disallowed ``None`` to be passed for ``emoji`` parameter in :meth:`Client.wait_for_reaction`." msgstr ":meth:`Client.wait_for_reaction` の ``emoji`` パラメータに ``None`` を渡すことを許可しないバグを修正しました。" -#: ../../whats_new.rst:1216 +#: ../../whats_new.rst:1249 msgid "v0.14.1" msgstr "v0.14.1" -#: ../../whats_new.rst:1219 +#: ../../whats_new.rst:1252 msgid "Bug fixes" msgstr "バグ修正" -#: ../../whats_new.rst:1222 +#: ../../whats_new.rst:1255 msgid "Fix bug with ``Reaction`` not being visible at import." msgstr "インポート時に ``Reaction`` が表示されないバグを修正しました。" -#: ../../whats_new.rst:1222 +#: ../../whats_new.rst:1255 msgid "This was also breaking the documentation." msgstr "これは、ドキュメントにも影響を与えていました。" -#: ../../whats_new.rst:1227 +#: ../../whats_new.rst:1260 msgid "v0.14.0" msgstr "v0.14.0" -#: ../../whats_new.rst:1229 +#: ../../whats_new.rst:1262 msgid "This update adds new API features and a couple of bug fixes." msgstr "このアップデートには、新しいAPI機能といくつかのバグ修正が含まれています。" -#: ../../whats_new.rst:1234 +#: ../../whats_new.rst:1267 msgid "Add support for Manage Webhooks permission under :attr:`Permissions.manage_webhooks`" msgstr ":attr:`Permissions.manage_webhooks` の下にWebhookの管理の権限のサポートを追加しました。" -#: ../../whats_new.rst:1235 +#: ../../whats_new.rst:1268 msgid "Add support for ``around`` argument in 3.5+ :meth:`Client.logs_from`." msgstr "3.5+ :meth:`Client.logs_from` で ``around`` 引数のサポートを追加しました。" -#: ../../whats_new.rst:1243 +#: ../../whats_new.rst:1276 msgid "Add support for reactions." msgstr "リアクションのサポートを追加します。" -#: ../../whats_new.rst:1237 +#: ../../whats_new.rst:1270 msgid ":meth:`Client.add_reaction` to add a reactions" msgstr "リアクションを追加する :meth:`Client.add_reaction`" -#: ../../whats_new.rst:1238 +#: ../../whats_new.rst:1271 msgid ":meth:`Client.remove_reaction` to remove a reaction." msgstr "リアクションを除去する :meth:`Client.remove_reaction`" -#: ../../whats_new.rst:1239 +#: ../../whats_new.rst:1272 msgid ":meth:`Client.get_reaction_users` to get the users that reacted to a message." msgstr "メッセージにリアクションしたユーザーを取得する :meth:`Client.get_reaction_users`" -#: ../../whats_new.rst:1240 +#: ../../whats_new.rst:1273 msgid ":attr:`Permissions.add_reactions` permission bit support." msgstr ":attr:`Permissions.add_reactions` パーミッションビットのサポート。" -#: ../../whats_new.rst:1241 +#: ../../whats_new.rst:1274 msgid "Two new events, :func:`on_reaction_add` and :func:`on_reaction_remove`." msgstr "2つの新しいイベント、 :func:`on_reaction_add` と :func:`on_reaction_remove` 。" -#: ../../whats_new.rst:1242 +#: ../../whats_new.rst:1275 msgid ":attr:`Message.reactions` to get reactions from a message." msgstr "メッセージからリアクションを取得する :attr:`Message.reactions`" -#: ../../whats_new.rst:1243 +#: ../../whats_new.rst:1276 msgid ":meth:`Client.wait_for_reaction` to wait for a reaction from a user." msgstr "ユーザーからのリアクションを待つ :meth:`Client.wait_for_reaction`" -#: ../../whats_new.rst:1248 +#: ../../whats_new.rst:1281 msgid "Fix bug with Paginator still allowing lines that are too long." msgstr "Paginatorが長すぎる行をいまだ許可していたバグを修正しました。" -#: ../../whats_new.rst:1249 +#: ../../whats_new.rst:1282 msgid "Fix the :attr:`Permissions.manage_emojis` bit being incorrect." msgstr ":attr:`Permissions.manage_emojis` ビットが正しくないバグを修正しました。" -#: ../../whats_new.rst:1254 +#: ../../whats_new.rst:1287 msgid "v0.13.0" msgstr "v0.13.0" -#: ../../whats_new.rst:1256 +#: ../../whats_new.rst:1289 msgid "This is a backwards compatible update with new features." msgstr "これは、新しい機能を備えた後方互換性のあるアップデートです。" -#: ../../whats_new.rst:1261 +#: ../../whats_new.rst:1294 msgid "Add the ability to manage emojis." msgstr "絵文字を管理する機能を追加しました。" -#: ../../whats_new.rst:1263 +#: ../../whats_new.rst:1296 msgid ":meth:`Client.create_custom_emoji` to create new emoji." msgstr "新しい絵文字を作成する :meth:`Client.create_custom_emoji` 。" -#: ../../whats_new.rst:1264 +#: ../../whats_new.rst:1297 msgid ":meth:`Client.edit_custom_emoji` to edit an old emoji." msgstr "既存の絵文字を編集する :meth:`Client.edit_custom_emoji` 。" -#: ../../whats_new.rst:1265 +#: ../../whats_new.rst:1298 msgid ":meth:`Client.delete_custom_emoji` to delete a custom emoji." msgstr "カスタム絵文字を削除する :meth:`Client.delete_custom_emoji` 。" -#: ../../whats_new.rst:1266 +#: ../../whats_new.rst:1299 msgid "Add new :attr:`Permissions.manage_emojis` toggle." msgstr "新しい :attr:`Permissions.manage_emoji` トグルを追加しました。" -#: ../../whats_new.rst:1268 +#: ../../whats_new.rst:1301 msgid "This applies for :class:`PermissionOverwrite` as well." msgstr "これは :class:`PermissionOverwrite` にも適用されます。" -#: ../../whats_new.rst:1269 +#: ../../whats_new.rst:1302 msgid "Add new statuses for :class:`Status`." msgstr ":class:`Status` に新しいステータスを追加しました。" -#: ../../whats_new.rst:1271 +#: ../../whats_new.rst:1304 msgid ":attr:`Status.dnd` (aliased with :attr:`Status.do_not_disturb`\\) for Do Not Disturb." msgstr "取り込み中を示す :attr:`Status.dnd` (エイリアス :attr:`Status.do_not_interrup` )" -#: ../../whats_new.rst:1272 +#: ../../whats_new.rst:1305 msgid ":attr:`Status.invisible` for setting your status to invisible (please see the docs for a caveat)." msgstr "ステータスを非表示に設定するための :attr:`Status.invisible` (ドキュメントの注意事項を参照してください)。" -#: ../../whats_new.rst:1273 +#: ../../whats_new.rst:1306 msgid "Deprecate :meth:`Client.change_status`" msgstr ":meth:`Client.change_status` を非推奨にしました。" -#: ../../whats_new.rst:1275 +#: ../../whats_new.rst:1308 msgid "Use :meth:`Client.change_presence` instead for better more up to date functionality." msgstr "より良い最新の機能を使用するためには、 :meth:`Client.change_presence` を使用してください。" -#: ../../whats_new.rst:1276 +#: ../../whats_new.rst:1309 msgid "This method is subject for removal in a future API version." msgstr "このメソッドは、将来の API バージョンで削除の対象となります。" -#: ../../whats_new.rst:1277 +#: ../../whats_new.rst:1310 msgid "Add :meth:`Client.change_presence` for changing your status with the new Discord API change." msgstr "新しい Discord API でステータスを変更するための :meth:`Client.change_presence` を追加しました。" -#: ../../whats_new.rst:1279 +#: ../../whats_new.rst:1312 msgid "This is the only method that allows changing your status to invisible or do not disturb." msgstr "これは、ステータスを非表示や取り込み中に変更できる唯一の方法です。" -#: ../../whats_new.rst:1284 +#: ../../whats_new.rst:1317 msgid "Paginator pages do not exceed their max_size anymore (:issue:`340`)" msgstr "ページネータのページがmax_sizeを超えないようにしました。 (:issue:`340`)" -#: ../../whats_new.rst:1285 +#: ../../whats_new.rst:1318 msgid "Do Not Disturb users no longer show up offline due to the new :class:`Status` changes." msgstr "取り込み中ユーザーは新しい :class:`Status` の変更によりこれ以降オフラインとして表示されないようになりました。" -#: ../../whats_new.rst:1290 +#: ../../whats_new.rst:1323 msgid "v0.12.0" msgstr "v0.12.0" -#: ../../whats_new.rst:1292 +#: ../../whats_new.rst:1325 msgid "This is a bug fix update that also comes with new features." msgstr "これは、新機能つきのバグ修正アップデートです。" -#: ../../whats_new.rst:1297 +#: ../../whats_new.rst:1330 msgid "Add custom emoji support." msgstr "カスタム絵文字サポートを追加しました。" -#: ../../whats_new.rst:1299 +#: ../../whats_new.rst:1332 msgid "Adds a new class to represent a custom Emoji named :class:`Emoji`" msgstr ":class:`Emoji` という名前のカスタム絵文字を表す新しいクラスを追加しました。" -#: ../../whats_new.rst:1300 +#: ../../whats_new.rst:1333 msgid "Adds a utility generator function, :meth:`Client.get_all_emojis`." msgstr "ユーティリティジェネレータ関数 :meth:`Client.get_all_emojis` を追加しました。" -#: ../../whats_new.rst:1301 +#: ../../whats_new.rst:1334 msgid "Adds a list of emojis on a server, :attr:`Server.emojis`." msgstr "サーバーの絵文字のリストを取得する :attr:`Server.emojis` を追加しました。" -#: ../../whats_new.rst:1302 +#: ../../whats_new.rst:1335 msgid "Adds a new event, :func:`on_server_emojis_update`." msgstr "新しいイベント :func:`on_server_emojis_update` を追加しました。" -#: ../../whats_new.rst:1303 +#: ../../whats_new.rst:1336 msgid "Add new server regions to :class:`ServerRegion`" msgstr ":class:`ServerRegion` に新しいサーバーリージョンを追加しました。" -#: ../../whats_new.rst:1305 +#: ../../whats_new.rst:1338 msgid ":attr:`ServerRegion.eu_central` and :attr:`ServerRegion.eu_west`." msgstr ":attr:`ServerRegion.eu_central` と :attr:`ServerRegion.eu_west` 。" -#: ../../whats_new.rst:1306 +#: ../../whats_new.rst:1339 msgid "Add support for new pinned system message under :attr:`MessageType.pins_add`." msgstr ":attr:`MessageType.pins_add` にて新しいピン留めのシステムメッセージのサポートを追加しました。" -#: ../../whats_new.rst:1307 +#: ../../whats_new.rst:1340 msgid "Add order comparisons for :class:`Role` to allow it to be compared with regards to hierarchy." msgstr ":class:`Role` への比較を追加し、階層を考慮した比較ができるようにしました。" -#: ../../whats_new.rst:1309 +#: ../../whats_new.rst:1342 msgid "This means that you can now do ``role_a > role_b`` etc to check if ``role_b`` is lower in the hierarchy." msgstr "つまり、 ``role_a > role_b`` などを実行して、階層内で ``role_b`` が低いかどうかを確認できるようになりました。" -#: ../../whats_new.rst:1311 +#: ../../whats_new.rst:1344 msgid "Add :attr:`Server.role_hierarchy` to get the server's role hierarchy." msgstr "サーバーのロール階層を取得する :attr:`Server.role_hierarchy` を追加しました。" -#: ../../whats_new.rst:1312 +#: ../../whats_new.rst:1345 msgid "Add :attr:`Member.server_permissions` to get a member's server permissions without their channel specific overwrites." msgstr "チャンネル固有の上書きなしでメンバーのサーバー権限を取得する :attr:`Member.server_permissions` を追加しました。" -#: ../../whats_new.rst:1313 +#: ../../whats_new.rst:1346 msgid "Add :meth:`Client.get_user_info` to retrieve a user's info from their ID." msgstr "IDからユーザ情報を取得することができる、 :meth:`Client.get_user_info` を追加しました。" -#: ../../whats_new.rst:1314 +#: ../../whats_new.rst:1347 msgid "Add a new ``Player`` property, ``Player.error`` to fetch the error that stopped the player." msgstr "プレイヤーを停止させたエラーを取得するために、新しい ``Player`` プロパティ ``Player.error`` を追加しました。" -#: ../../whats_new.rst:1316 +#: ../../whats_new.rst:1349 msgid "To help with this change, a player's ``after`` function can now take a single parameter denoting the current player." msgstr "この変更とともに、プレイヤーの ``after`` 関数に現在のプレイヤーを示すパラメータを取ることができるようになりました。" -#: ../../whats_new.rst:1317 +#: ../../whats_new.rst:1350 msgid "Add support for server verification levels." msgstr "サーバー認証レベルのサポートを追加しました。" -#: ../../whats_new.rst:1319 +#: ../../whats_new.rst:1352 msgid "Adds a new enum called :class:`VerificationLevel`." msgstr ":class:`VerificationLevel` という新しい列挙型を追加しました。" -#: ../../whats_new.rst:1320 +#: ../../whats_new.rst:1353 msgid "This enum can be used in :meth:`Client.edit_server` under the ``verification_level`` keyword argument." msgstr "この列挙型は、 :meth:`Client.edit_server` の ``verification_level`` キーワード引数で使用できます。" -#: ../../whats_new.rst:1321 +#: ../../whats_new.rst:1354 msgid "Adds a new attribute in the server, :attr:`Server.verification_level`." msgstr "サーバーに :attr:`Server.verification_level` という新しい属性を追加しました。" -#: ../../whats_new.rst:1322 +#: ../../whats_new.rst:1355 msgid "Add :attr:`Server.voice_client` shortcut property for :meth:`Client.voice_client_in`." msgstr ":meth:`Client.voice_client_in` のショートカットプロパティである :attr:`Server.voice_client` を追加しました。" -#: ../../whats_new.rst:1324 +#: ../../whats_new.rst:1357 msgid "This is technically old (was added in v0.10.0) but was undocumented until v0.12.0." msgstr "これは厳密にいえば過去のもの (v0.10.0で追加) ですが、v0.12.0までは文書化されていませんでした。" -#: ../../whats_new.rst:1326 -#: ../../whats_new.rst:1372 +#: ../../whats_new.rst:1359 +#: ../../whats_new.rst:1405 msgid "For the command extension, the following are new:" msgstr "コマンド拡張機能では、以下の新機能が追加されました:" -#: ../../whats_new.rst:1328 +#: ../../whats_new.rst:1361 msgid "Add custom emoji converter." msgstr "カスタム絵文字コンバータを追加しました。" -#: ../../whats_new.rst:1329 +#: ../../whats_new.rst:1362 msgid "All default converters that can take IDs can now convert via ID." msgstr "IDを取ることができるすべてのデフォルトのコンバータが、IDにより変換することができるようにしました。" -#: ../../whats_new.rst:1330 +#: ../../whats_new.rst:1363 msgid "Add coroutine support for ``Bot.command_prefix``." msgstr "``Bot.command_prefix`` にコルーチンサポートを追加しました。" -#: ../../whats_new.rst:1331 +#: ../../whats_new.rst:1364 msgid "Add a method to reset command cooldown." msgstr "コマンドのクールダウンをリセットするメソッドを追加しました。" -#: ../../whats_new.rst:1336 +#: ../../whats_new.rst:1369 msgid "Fix bug that caused the library to not work with the latest ``websockets`` library." msgstr "最新の ``websockets`` ライブラリでライブラリが動作しないバグを修正しました。" -#: ../../whats_new.rst:1337 +#: ../../whats_new.rst:1370 msgid "Fix bug that leaked keep alive threads (:issue:`309`)" msgstr "キープアライブスレッドをリークしていたバグを修正しました。 (:issue:`309`)" -#: ../../whats_new.rst:1338 +#: ../../whats_new.rst:1371 msgid "Fix bug that disallowed :class:`ServerRegion` from being used in :meth:`Client.edit_server`." msgstr ":meth:`Client.edit_server` で :class:`ServerRegion` が使用できないバグを修正しました。" -#: ../../whats_new.rst:1339 +#: ../../whats_new.rst:1372 msgid "Fix bug in :meth:`Channel.permissions_for` that caused permission resolution to happen out of order." msgstr ":meth:`Channel.permissions_for` で権限解決が誤った順序で行われたバグを修正しました。" -#: ../../whats_new.rst:1340 +#: ../../whats_new.rst:1373 msgid "Fix bug in :attr:`Member.top_role` that did not account for same-position roles." msgstr ":attr:`Member.top_role` が同じポジションの役割を考慮しないバグを修正しました。" -#: ../../whats_new.rst:1345 +#: ../../whats_new.rst:1378 msgid "v0.11.0" msgstr "v0.11.0" -#: ../../whats_new.rst:1347 +#: ../../whats_new.rst:1380 msgid "This is a minor bug fix update that comes with a gateway update (v5 -> v6)." msgstr "これはゲートウェイのアップデート (v5 -> v6) を含むマイナーなバグ修正アップデートです。" -#: ../../whats_new.rst:1350 +#: ../../whats_new.rst:1383 msgid "Breaking Changes" msgstr "破壊的変更" -#: ../../whats_new.rst:1352 +#: ../../whats_new.rst:1385 msgid "``Permissions.change_nicknames`` has been renamed to :attr:`Permissions.change_nickname` to match the UI." msgstr "``Permissions.change_nicknames`` は UIに一致するように :attr:`Permissions.change_nickname` に名前が変更されました。" -#: ../../whats_new.rst:1357 +#: ../../whats_new.rst:1390 msgid "Add the ability to prune members via :meth:`Client.prune_members`." msgstr ":meth:`Client.prune_members` でメンバーを一括キックする機能を追加しました。" -#: ../../whats_new.rst:1358 +#: ../../whats_new.rst:1391 msgid "Switch the websocket gateway version to v6 from v5. This allows the library to work with group DMs and 1-on-1 calls." msgstr "WebSocketゲートウェイのバージョンをv5からv6に切り替えました。これにより、ライブラリはグループDMと1-on-1コールで動作するようになります。" -#: ../../whats_new.rst:1359 +#: ../../whats_new.rst:1392 msgid "Add :attr:`AppInfo.owner` attribute." msgstr ":attr:`AppInfo.owner` 属性を追加しました。" -#: ../../whats_new.rst:1360 +#: ../../whats_new.rst:1393 msgid "Add :class:`CallMessage` for group voice call messages." msgstr "グループボイス通話メッセージを示す :class:`CallMessage` を追加しました。" -#: ../../whats_new.rst:1361 +#: ../../whats_new.rst:1394 msgid "Add :class:`GroupCall` for group voice call information." msgstr "グループボイス通話情報を示す :class:`GroupCall` を追加しました。" -#: ../../whats_new.rst:1362 +#: ../../whats_new.rst:1395 msgid "Add :attr:`Message.system_content` to get the system message." msgstr "システムメッセージを取得する :attr:`Message.system_content` を追加しました。" -#: ../../whats_new.rst:1363 +#: ../../whats_new.rst:1396 msgid "Add the remaining VIP servers and the Brazil servers into :class:`ServerRegion` enum." msgstr "残りのVIPサーバーとブラジルサーバーを :class:`ServerRegion` に追加しました。" -#: ../../whats_new.rst:1364 +#: ../../whats_new.rst:1397 msgid "Add ``stderr`` argument to :meth:`VoiceClient.create_ffmpeg_player` to redirect stderr." msgstr ":meth:`VoiceClient.create_ffmpeg_player` に標準エラー出力をリダイレクトするための ``stderr`` 引数を追加しました。" -#: ../../whats_new.rst:1365 +#: ../../whats_new.rst:1398 msgid "The library now handles implicit permission resolution in :meth:`Channel.permissions_for`." msgstr "ライブラリは :meth:`Channel.permissions_for` で暗黙的な権限解決を処理するようになりました。" -#: ../../whats_new.rst:1366 +#: ../../whats_new.rst:1399 msgid "Add :attr:`Server.mfa_level` to query a server's 2FA requirement." msgstr "サーバーの 2FA 要件を取得する :attr:`Server.mfa_level` を追加しました。" -#: ../../whats_new.rst:1367 +#: ../../whats_new.rst:1400 msgid "Add :attr:`Permissions.external_emojis` permission." msgstr ":attr:`Permissions.external_emojis` 権限を追加しました。" -#: ../../whats_new.rst:1368 +#: ../../whats_new.rst:1401 msgid "Add :attr:`Member.voice` attribute that refers to a :class:`VoiceState`." msgstr ":class:`VoiceState` を返す :attr:`Member.voice` 属性を追加しました。" -#: ../../whats_new.rst:1370 +#: ../../whats_new.rst:1403 msgid "For backwards compatibility, the member object will have properties mirroring the old behaviour." msgstr "後方互換性のため、メンバーオブジェクトには古い挙動をミラーリングするプロパティも存在します。" -#: ../../whats_new.rst:1374 +#: ../../whats_new.rst:1407 msgid "Command cooldown system with the ``cooldown`` decorator." msgstr "``cololdown`` デコレータを用いたコマンドクールダウンシステム。" -#: ../../whats_new.rst:1375 +#: ../../whats_new.rst:1408 msgid "``UserInputError`` exception for the hierarchy for user input related errors." msgstr "ユーザー入力関連エラーの親である ``UserInputError`` 例外。" -#: ../../whats_new.rst:1380 +#: ../../whats_new.rst:1413 msgid ":attr:`Client.email` is now saved when using a token for user accounts." msgstr ":attr:`Client.email` がユーザーアカウントにトークンを使用してログインしたとき保存されるようになりました。" -#: ../../whats_new.rst:1381 +#: ../../whats_new.rst:1414 msgid "Fix issue when removing roles out of order." msgstr "順番になってないロールの除去で発生した問題を修正しました。" -#: ../../whats_new.rst:1382 +#: ../../whats_new.rst:1415 msgid "Fix bug where discriminators would not update." msgstr "タグが更新されないバグを修正しました。" -#: ../../whats_new.rst:1383 +#: ../../whats_new.rst:1416 msgid "Handle cases where ``HEARTBEAT`` opcode is received. This caused bots to disconnect seemingly randomly." msgstr "``HEARTBEAT`` のコードを受け取った場合を処理するようにしました。これは、ボットが一見ランダムに切断されるのを引き起こしていました。" -#: ../../whats_new.rst:1385 +#: ../../whats_new.rst:1418 msgid "For the command extension, the following bug fixes apply:" msgstr "コマンド拡張機能では、以下のバグが修正されました:" -#: ../../whats_new.rst:1387 +#: ../../whats_new.rst:1420 msgid "``Bot.check`` decorator is actually a decorator not requiring parentheses." msgstr "``Bot.check`` デコレータが実際に括弧を必要としないようになりました。" -#: ../../whats_new.rst:1388 +#: ../../whats_new.rst:1421 msgid "``Bot.remove_command`` and ``Group.remove_command`` no longer throw if the command doesn't exist." msgstr "``Bot.remove_command`` と ``Group.remove_command`` が、コマンドが存在しない場合に例外を送出しないようにしました。" -#: ../../whats_new.rst:1389 +#: ../../whats_new.rst:1422 msgid "Command names are no longer forced to be ``lower()``." msgstr "コマンド名は強制的に ``lower()`` されなくなりました。" -#: ../../whats_new.rst:1390 +#: ../../whats_new.rst:1423 msgid "Fix a bug where Member and User converters failed to work in private message contexts." msgstr "MemberとUserのコンバータがプライベートメッセージ内で動かなかったバグを修正しました。" -#: ../../whats_new.rst:1391 +#: ../../whats_new.rst:1424 msgid "``HelpFormatter`` now ignores hidden commands when deciding the maximum width." msgstr "``HelpFormatter`` が最大幅を決めるときに隠されたコマンドを無視するようになりました。" -#: ../../whats_new.rst:1396 +#: ../../whats_new.rst:1429 msgid "v0.10.0" msgstr "v0.10.0" -#: ../../whats_new.rst:1398 +#: ../../whats_new.rst:1431 msgid "For breaking changes, see :ref:`migrating-to-async`. The breaking changes listed there will not be enumerated below. Since this version is rather a big departure from v0.9.2, this change log will be non-exhaustive." msgstr "破壊的変更に関しては、 :ref:`migrating-to-async` を参照してください。そのページで列挙された破壊的変更はここでは述べません。このバージョンがv0.9.2よりかなり大きな変更であるため、変更履歴は完全ではありません。" -#: ../../whats_new.rst:1403 +#: ../../whats_new.rst:1436 msgid "The library is now fully ``asyncio`` compatible, allowing you to write non-blocking code a lot more easily." msgstr "ライブラリが完全に ``asyncio`` に対応するようになり、ノンブロッキングコードをより簡単に書けるようになりました。" -#: ../../whats_new.rst:1404 +#: ../../whats_new.rst:1437 msgid "The library now fully handles 429s and unconditionally retries on 502s." msgstr "ライブラリが429を完全に処理し、502で無条件に再試行するようにしました。" -#: ../../whats_new.rst:1405 +#: ../../whats_new.rst:1438 msgid "A new command extension module was added but is currently undocumented. Figuring it out is left as an exercise to the reader." msgstr "新しいコマンド拡張機能モジュールが追加されましたが、現在文書化されていません。詳細は読者が自身で調べることをおすすめします。" -#: ../../whats_new.rst:1406 +#: ../../whats_new.rst:1439 msgid "Two new exception types, :exc:`Forbidden` and :exc:`NotFound` to denote permission errors or 404 errors." msgstr "パーミッションエラーや404エラーを示す2つの新しい例外タイプ、 :exc:`Forbidden` と :exc:`NotFound` が追加されました。" -#: ../../whats_new.rst:1407 +#: ../../whats_new.rst:1440 msgid "Added :meth:`Client.delete_invite` to revoke invites." msgstr "招待を取り消す :meth:`Client.delete_invite` を追加しました。" -#: ../../whats_new.rst:1408 +#: ../../whats_new.rst:1441 msgid "Added support for sending voice. Check :class:`VoiceClient` for more details." msgstr "音声を送信するためのサポートを追加しました。詳細は :class:`VoiceClient` を参照してください。" -#: ../../whats_new.rst:1409 +#: ../../whats_new.rst:1442 msgid "Added :meth:`Client.wait_for_message` coroutine to aid with follow up commands." msgstr "フォローアップコマンドを作りやすいように、コルーチン :meth:`Client.wait_for_message` を追加しました。" -#: ../../whats_new.rst:1410 +#: ../../whats_new.rst:1443 msgid "Added :data:`version_info` named tuple to check version info of the library." msgstr "ライブラリのバージョン情報を確認するための、namedtuple :data:`version_info` を追加しました。" -#: ../../whats_new.rst:1411 +#: ../../whats_new.rst:1444 msgid "Login credentials are now cached to have a faster login experience. You can disable this by passing in ``cache_auth=False`` when constructing a :class:`Client`." msgstr "ログイン情報をキャッシュすることで、より高速にログインできるようになりました。これを無効にするには、 :class:`Client` を作成する際に ``cache_auth=False`` を渡します。" -#: ../../whats_new.rst:1413 +#: ../../whats_new.rst:1446 msgid "New utility function, :func:`discord.utils.get` to simplify retrieval of items based on attributes." msgstr "新しいユーティリティ関数 :func:`discord.utils.get` は、属性に基づいたアイテムの取得を簡素化します。" -#: ../../whats_new.rst:1414 +#: ../../whats_new.rst:1447 msgid "All data classes now support ``!=``, ``==``, ``hash(obj)`` and ``str(obj)``." msgstr "すべてのデータクラスが ``!=``, ``==``, ``hash(obj)``, ``str(obj)`` をサポートするようになりました" -#: ../../whats_new.rst:1415 +#: ../../whats_new.rst:1448 msgid "Added :meth:`Client.get_bans` to get banned members from a server." msgstr "サーバーからBANされたメンバーを取得する :meth:`Client.get_bans` を追加しました。" -#: ../../whats_new.rst:1416 +#: ../../whats_new.rst:1449 msgid "Added :meth:`Client.invites_from` to get currently active invites in a server." msgstr "サーバーで現在アクティブな招待を取得する :meth:`Client.invites_from` を追加しました。" -#: ../../whats_new.rst:1417 +#: ../../whats_new.rst:1450 msgid "Added :attr:`Server.me` attribute to get the :class:`Member` version of :attr:`Client.user`." msgstr ":attr:`Client.user` の :class:`Member` を取得できる :attr:`Server.me` を追加しました。" -#: ../../whats_new.rst:1418 +#: ../../whats_new.rst:1451 msgid "Most data classes now support a ``hash(obj)`` function to allow you to use them in ``set`` or ``dict`` classes or subclasses." msgstr "ほとんどのデータクラスが ``hash(obj)`` 関数をサポートするようになり、 ``set`` や ``dict`` クラス、サブクラスで使用できるようになりました。" -#: ../../whats_new.rst:1419 +#: ../../whats_new.rst:1452 msgid "Add :meth:`Message.clean_content` to get a text version of the content with the user and channel mentioned changed into their names." msgstr "ユーザーとチャンネルのメンションを名前に変更したバージョンのコンテンツを取得する、 :meth:`Message.clean_content` を追加しました。" -#: ../../whats_new.rst:1420 +#: ../../whats_new.rst:1453 msgid "Added a way to remove the messages of the user that just got banned in :meth:`Client.ban`." msgstr ":meth:`Client.ban` でBANされたユーザーのメッセージを削除する方法を追加しました。" -#: ../../whats_new.rst:1421 +#: ../../whats_new.rst:1454 msgid "Added :meth:`Client.wait_until_ready` to facilitate easy creation of tasks that require the client cache to be ready." msgstr "クライアントキャッシュを準備する必要があるタスクを簡単に作成できるように、 :meth:`Client.wait_until_ready` を追加しました。" -#: ../../whats_new.rst:1422 +#: ../../whats_new.rst:1455 msgid "Added :meth:`Client.wait_until_login` to facilitate easy creation of tasks that require the client to be logged in." msgstr "クライアントのログインを必要とするタスクを簡単に作成できるように :meth:`Client.wait_until_login` を追加しました。" -#: ../../whats_new.rst:1423 +#: ../../whats_new.rst:1456 msgid "Add :class:`discord.Game` to represent any game with custom text to send to :meth:`Client.change_status`." msgstr ":class:`Client.change_status` に送信する、カスタムテキストを含む任意のゲームを表す :meth:`discord.Game` を追加しました。" -#: ../../whats_new.rst:1424 +#: ../../whats_new.rst:1457 msgid "Add :attr:`Message.nonce` attribute." msgstr ":attr:`Message.nonce` 属性を追加しました。" -#: ../../whats_new.rst:1425 +#: ../../whats_new.rst:1458 msgid "Add :meth:`Member.permissions_in` as another way of doing :meth:`Channel.permissions_for`." msgstr ":meth:`Channel.permissions_for` の代替として :meth:`Member.permissions_in` を追加しました。" -#: ../../whats_new.rst:1426 +#: ../../whats_new.rst:1459 msgid "Add :meth:`Client.move_member` to move a member to another voice channel." msgstr "メンバーを別のボイスチャンネルに移動するための :meth:`Client.move_member` を追加しました。" -#: ../../whats_new.rst:1427 +#: ../../whats_new.rst:1460 msgid "You can now create a server via :meth:`Client.create_server`." msgstr ":meth:`Client.create_server` を使用してサーバーを作成できるようになりました。" -#: ../../whats_new.rst:1428 +#: ../../whats_new.rst:1461 msgid "Added :meth:`Client.edit_server` to edit existing servers." msgstr "既存のサーバを編集するための :meth:`Client.edit_server` を追加しました。" -#: ../../whats_new.rst:1429 +#: ../../whats_new.rst:1462 msgid "Added :meth:`Client.server_voice_state` to server mute or server deafen a member." msgstr "メンバーをサーバーミュートしたり、サーバースピーカーミュートしたりできる :meth:`Client.server_voice_state` を追加しました。" -#: ../../whats_new.rst:1430 +#: ../../whats_new.rst:1463 msgid "If you are being rate limited, the library will now handle it for you." msgstr "レートリミットの際にライブラリが処理するようになりました。" -#: ../../whats_new.rst:1431 +#: ../../whats_new.rst:1464 msgid "Add :func:`on_member_ban` and :func:`on_member_unban` events that trigger when a member is banned/unbanned." msgstr "メンバーがBANまたはBAN解除されたときに実行される :func:`on_member_ban` と :func:`on_member_unban` イベントを追加しました。" -#: ../../whats_new.rst:1434 +#: ../../whats_new.rst:1467 msgid "Performance Improvements" msgstr "パフォーマンスの改善" -#: ../../whats_new.rst:1436 +#: ../../whats_new.rst:1469 msgid "All data classes now use ``__slots__`` which greatly reduce the memory usage of things kept in cache." msgstr "すべてのデータクラスは ``__slots__`` を使用するようになり、キャッシュに保存されているもののメモリ使用量を大幅に削減しました。" -#: ../../whats_new.rst:1437 +#: ../../whats_new.rst:1470 msgid "Due to the usage of ``asyncio``, the CPU usage of the library has gone down significantly." msgstr "``asyncio`` の使用により、ライブラリの CPU 使用率は大幅に減少しました。" -#: ../../whats_new.rst:1438 +#: ../../whats_new.rst:1471 msgid "A lot of the internal cache lists were changed into dictionaries to change the ``O(n)`` lookup into ``O(1)``." msgstr "多くの内部キャッシュリストが ``O(n)`` 検索を ``O(1)`` に変更するために辞書型に変更されました。" -#: ../../whats_new.rst:1439 +#: ../../whats_new.rst:1472 msgid "Compressed READY is now on by default. This means if you're on a lot of servers (or maybe even a few) you would receive performance improvements by having to download and process less data." msgstr "圧縮されたREADYがデフォルトでオンになりました。 つまり、多くのサーバー(あるいはもしかすると少なめのサーバー) にいる場合、より少ないデータをダウンロードして処理することでパフォーマンスが向上されます。" -#: ../../whats_new.rst:1441 +#: ../../whats_new.rst:1474 msgid "While minor, change regex from ``\\d+`` to ``[0-9]+`` to avoid unnecessary unicode character lookups." msgstr "小規模ながら、不要な Unicode 文字の検索を避けるために正規表現を ``\\d+`` から ``[0-9]+`` に変更しました。" -#: ../../whats_new.rst:1446 +#: ../../whats_new.rst:1479 msgid "Fix bug where guilds being updated did not edit the items in cache." msgstr "ギルドが更新されてもキャッシュ内のアイテムが編集されなかったバグを修正しました。" -#: ../../whats_new.rst:1447 +#: ../../whats_new.rst:1480 msgid "Fix bug where ``member.roles`` were empty upon joining instead of having the ``@everyone`` role." msgstr "``member.roles`` が参加時に ``@everyone`` ロールを有さず空であったバグを修正しました。" -#: ../../whats_new.rst:1448 +#: ../../whats_new.rst:1481 msgid "Fix bug where :meth:`Role.is_everyone` was not being set properly when the role was being edited." msgstr "ロールが編集されたときに :meth:`Role.is_everyone` が正しく設定されていないバグを修正しました。" -#: ../../whats_new.rst:1449 +#: ../../whats_new.rst:1482 msgid ":meth:`Client.logs_from` now handles cases where limit > 100 to sidestep the discord API limitation." msgstr ":meth:`Client.logs_from` が、DiscordのAPI制限を避けるために制限 > 100を超える場合を処理するようになりました。" -#: ../../whats_new.rst:1450 +#: ../../whats_new.rst:1483 msgid "Fix bug where a role being deleted would trigger a ``ValueError``." msgstr "ロールが削除されると、 ``ValueError`` が発生するバグを修正しました。" -#: ../../whats_new.rst:1451 +#: ../../whats_new.rst:1484 msgid "Fix bug where :meth:`Permissions.kick_members` and :meth:`Permissions.ban_members` were flipped." msgstr ":meth:`Permissions.kick_members` と :meth:`Permissions.ban_members` がひっくり返されたバグを修正しました。" -#: ../../whats_new.rst:1452 +#: ../../whats_new.rst:1485 msgid "Mentions are now triggered normally. This was changed due to the way discord handles it internally." msgstr "メンションが正常に発動されるようになりました。これは、Discordの内部処理の方法の変更によるものです。" -#: ../../whats_new.rst:1453 +#: ../../whats_new.rst:1486 msgid "Fix issue when a :class:`Message` would attempt to upgrade a :attr:`Message.server` when the channel is a :class:`Object`." msgstr "チャンネルが :class:`Object` の時に、 :class:`Message` が :attr:`Message.server` をアップグレードしようとする問題を修正しました。" -#: ../../whats_new.rst:1455 +#: ../../whats_new.rst:1488 msgid "Unavailable servers were not being added into cache, this has been corrected." msgstr "利用できないサーバーがキャッシュに追加されない不具合が修正されました。" diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 5a3bee84dc1e..44db8c3d4042 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,369 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p5p2: + +v2.5.2 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix a serialization issue when sending embeds (:issue:`10126`) + +.. _vp2p5p1: + +v2.5.1 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix :attr:`InteractionCallbackResponse.resource` having incorrect state (:issue:`10107`) +- Create :class:`ScheduledEvent` on cache miss for :func:`on_scheduled_event_delete` (:issue:`10113`) +- Add defaults for :class:`Message` creation preventing some crashes (:issue:`10115`) +- Fix :meth:`Attachment.is_spoiler` and :meth:`Attachment.is_voice_message` being incorrect (:issue:`10122`) + + +.. _vp2p5p0: + +v2.5.0 +------- + +New Features +~~~~~~~~~~~~~ + +- Add support for message forwarding (:issue:`9950`) + - Adds :class:`MessageReferenceType` + - Adds :class:`MessageSnapshot` + - Adds ``type`` parameter to :class:`MessageReference`, :meth:`MessageReference.from_message`, and :meth:`PartialMessage.to_reference` + - Add :meth:`PartialMessage.forward` + +- Add SKU subscriptions support (:issue:`9930`) + - Adds new events :func:`on_subscription_create`, :func:`on_subscription_update`, and :func:`on_subscription_delete` + - Add :class:`SubscriptionStatus` enum + - Add :class:`Subscription` model + - Add :meth:`SKU.fetch_subscription` and :meth:`SKU.subscriptions` + +- Add support for application emojis (:issue:`9891`) + - Add :meth:`Client.create_application_emoji` + - Add :meth:`Client.fetch_application_emoji` + - Add :meth:`Client.fetch_application_emojis` + - Add :meth:`Emoji.is_application_owned` + +- Support for Soundboard and VC effects (:issue:`9349`) + - Add :class:`BaseSoundboardSound`, :class:`SoundboardDefaultSound`, and :class:`SoundboardSound` + - Add :class:`VoiceChannelEffect` + - Add :class:`VoiceChannelEffectAnimation` + - Add :class:`VoiceChannelEffectAnimationType` + - Add :class:`VoiceChannelSoundEffect` + - Add :meth:`VoiceChannel.send_sound` + - Add new audit log actions: :attr:`AuditLogAction.soundboard_sound_create`, :attr:`AuditLogAction.soundboard_sound_update`, and :attr:`AuditLogAction.soundboard_sound_delete`. + - Add :attr:`Intents.expressions` and make :attr:`Intents.emojis` and :attr:`Intents.emojis_and_stickers` aliases of that intent. + - Add new events: :func:`on_soundboard_sound_create`, :func:`on_soundboard_sound_update`, :func:`on_soundboard_sound_delete`, and :func:`on_voice_channel_effect`. + - Add methods and properties dealing with soundboards: + - :attr:`Client.soundboard_sounds` + - :attr:`Guild.soundboard_sounds` + - :meth:`Client.get_soundboard_sound` + - :meth:`Guild.get_soundboard_sound` + - :meth:`Client.fetch_soundboard_default_sounds` + - :meth:`Guild.fetch_soundboard_sound` + - :meth:`Guild.fetch_soundboard_sounds` + - :meth:`Guild.create_soundboard_sound` + +- Add support for retrieving interaction responses when sending a response (:issue:`9957`) + - Methods from :class:`InteractionResponse` now return :class:`InteractionCallbackResponse` + - Depending on the interaction response type, :attr:`InteractionCallbackResponse.resource` will be different + +- Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`) +- Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`) +- Add richer :meth:`Role.move` interface (:issue:`10100`) +- Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`) +- Add new flags for :class:`AttachmentFlags` (:issue:`10085`) +- Add :func:`on_raw_presence_update` event that does not depend on cache state (:issue:`10048`) + - This requires setting the ``enable_raw_presences`` keyword argument within :class:`Client`. + +- Add :attr:`ForumChannel.members` property. (:issue:`10034`) +- Add ``exclude_deleted`` parameter to :meth:`Client.entitlements` (:issue:`10027`) +- Add :meth:`Client.fetch_guild_preview` (:issue:`9986`) +- Add :meth:`AutoShardedClient.fetch_session_start_limits` (:issue:`10007`) +- Add :attr:`PartialMessageable.mention` (:issue:`9988`) +- Add command target to :class:`MessageInteractionMetadata` (:issue:`10004`) + - :attr:`MessageInteractionMetadata.target_user` + - :attr:`MessageInteractionMetadata.target_message_id` + - :attr:`MessageInteractionMetadata.target_message` + +- Add :attr:`Message.forward` flag (:issue:`9978`) + +- Add support for purchase notification messages (:issue:`9906`) + - Add new type :attr:`MessageType.purchase_notification` + - Add new models :class:`GuildProductPurchase` and :class:`PurchaseNotification` + - Add :attr:`Message.purchase_notification` + +- Add ``category`` parameter to :meth:`.abc.GuildChannel.clone` (:issue:`9941`) +- Add support for message call (:issue:`9911`) + - Add new models :class:`CallMessage` + - Add :attr:`Message.call` attribute + +- Parse full message for message edit event (:issue:`10035`) + - Adds :attr:`RawMessageUpdateEvent.message` attribute + - Potentially speeds up :func:`on_message_edit` by no longer copying data + +- Add support for retrieving and editing integration type configuration (:issue:`9818`) + - This adds :class:`IntegrationTypeConfig` + - Retrievable via :attr:`AppInfo.guild_integration_config` and :attr:`AppInfo.user_integration_config`. + - Editable via :meth:`AppInfo.edit` + +- Allow passing ``None`` for ``scopes`` parameter in :func:`utils.oauth_url` (:issue:`10078`) +- Add support for :attr:`MessageType.poll_result` messages (:issue:`9905`) +- Add various new :class:`MessageFlags` +- Add :meth:`Member.fetch_voice` (:issue:`9908`) +- Add :attr:`Guild.dm_spam_detected_at` and :meth:`Guild.is_dm_spam_detected` (:issue:`9808`) +- Add :attr:`Guild.raid_detected_at` and :meth:`Guild.is_raid_detected` (:issue:`9808`) +- Add :meth:`Client.fetch_premium_sticker_pack` (:issue:`9909`) +- Add :attr:`AppInfo.approximate_user_install_count` (:issue:`9915`) +- Add :meth:`Guild.fetch_role` (:issue:`9921`) +- Add :attr:`Attachment.title` (:issue:`9904`) +- Add :attr:`Member.guild_banner` and :attr:`Member.display_banner` +- Re-add ``connector`` parameter that was removed during v2.0 (:issue:`9900`) +- |commands| Add :class:`~discord.ext.commands.SoundboardSoundConverter` (:issue:`9973`) + +Bug Fixes +~~~~~~~~~~ + +- Change the default file size limit for :attr:`Guild.filesize_limit` to match new Discord limit of 10 MiB (:issue:`10084`) +- Handle improper 1000 close code closures by Discord + - This fixes an issue causing excessive IDENTIFY in large bots + +- Fix potential performance regression when dealing with cookies in the library owned session (:issue:`9916`) +- Add support for AEAD XChaCha20 Poly1305 encryption mode (:issue:`9953`) + - This allows voice to continue working when the older encryption modes eventually get removed. + - Support for DAVE is still tentative. + +- Fix large performance regression due to polls when creating messages +- Fix cases where :attr:`Member.roles` contains a ``None`` role (:issue:`10093`) +- Update all channel clone implementations to work as expected (:issue:`9935`) +- Fix bug in :meth:`Client.entitlements` only returning 100 entries (:issue:`10051`) +- Fix :meth:`TextChannel.clone` always sending slowmode when not applicable to news channels (:issue:`9967`) +- Fix :attr:`Message.system_content` for :attr:`MessageType.role_subscription_purchase` renewals (:issue:`9955`) +- Fix :attr:`Sticker.url` for GIF stickers (:issue:`9913`) +- Fix :attr:`User.default_avatar` for team users and webhooks (:issue:`9907`) +- Fix potential rounding error in :attr:`Poll.duration` (:issue:`9903`) +- Fix introduced potential TypeError when raising :exc:`app_commands.CommandSyncFailure` +- Fix :attr:`AuditLogEntry.target` causing errors for :attr:`AuditLogAction.message_pin` and :attr:`AuditLogAction.message_unpin` actions (:issue:`10061`). +- Fix incorrect :class:`ui.Select` maximum option check (:issue:`9878`, :issue:`9879`) +- Fix path sanitisation for absolute Windows paths when using ``__main__`` (:issue:`10096`, :issue:`10097`) +- |tasks| Fix race condition when setting timer handle when using uvloop (:issue:`10020`) +- |commands| Fix issue with category cooldowns outside of guild channels (:issue:`9959`) +- |commands| Fix :meth:`Context.defer ` unconditionally deferring +- |commands| Fix callable FlagConverter defaults on hybrid commands not being called (:issue:`10037`) +- |commands| Unwrap :class:`~discord.ext.commands.Parameter` if given as default to :func:`~ext.commands.parameter` (:issue:`9977`) +- |commands| Fix fallback behaviour not being respected when calling replace for :class:`~.ext.commands.Parameter` (:issue:`10076`, :issue:`10077`) +- |commands| Respect ``enabled`` keyword argument for hybrid app commands (:issue:`10001`) + +Miscellaneous +~~~~~~~~~~~~~~ + +- Use a fallback package for ``audioop`` to allow the library to work in Python 3.13 or newer. +- Remove ``aiodns`` from being used on Windows (:issue:`9898`) +- Add zstd gateway compression to ``speed`` extras (:issue:`9947`) + - This can be installed using ``discord.py[speed]`` + +- Add proxy support fetching from the CDN (:issue:`9966`) +- Remove ``/`` from being safe from URI encoding when constructing paths internally +- Sanitize invite argument before calling the invite info endpoint +- Avoid returning in finally in specific places to prevent exception swallowing (:issue:`9981`, :issue:`9984`) +- Enforce and create random nonces when creating messages throughout the library +- Revert IPv6 block in the library (:issue:`9870`) +- Allow passing :class:`Permissions` object to :func:`app_commands.default_permissions` decorator (:issue:`9951`, :issue:`9971`) + + +.. _vp2p4p0: + +v2.4.0 +------- + +New Features +~~~~~~~~~~~~~ + +- Add support for allowed contexts in app commands (:issue:`9760`). + - An "allowed context" is the location where an app command can be used. + - This is an internal change to decorators such as :func:`app_commands.guild_only` and :func:`app_commands.dm_only`. + - Add :func:`app_commands.private_channel_only`. + - Add :func:`app_commands.allowed_contexts`. + - Add :class:`app_commands.AppCommandContext`. + - Add :attr:`app_commands.Command.allowed_contexts`. + - Add :attr:`app_commands.AppCommand.allowed_contexts`. + - Add :attr:`app_commands.ContextMenu.allowed_contexts`. + +- Add support for user-installable apps (:issue:`9760`). + - Add :attr:`app_commands.Command.allowed_installs`. + - Add :attr:`app_commands.AppCommand.allowed_installs`. + - Add :attr:`app_commands.ContextMenu.allowed_installs`. + - Add :func:`app_commands.allowed_installs`. + - Add :func:`app_commands.guild_install`. + - Add :func:`app_commands.user_install`. + - Add :class:`app_commands.AppInstallationType`. + - Add :attr:`Interaction.context`. + - Add :meth:`Interaction.is_guild_integration`. + - Add :meth:`Interaction.is_user_integration`. + +- Add support for Polls (:issue:`9759`). + - Polls can be created using :class:`Poll` and the ``poll`` keyword-only parameter in various message sending methods. + - Add :class:`PollAnswer` and :class:`PollMedia`. + - Add :attr:`Intents.polls`, :attr:`Intents.guild_polls` and :attr:`Intents.dm_polls` intents. + - Add :meth:`Message.end_poll` method to end polls. + - Add new events, :func:`on_poll_vote_add`, :func:`on_poll_vote_remove`, :func:`on_raw_poll_vote_add`, and :func:`on_raw_poll_vote_remove`. + +- Voice handling has been completely rewritten to hopefully fix many bugs (:issue:`9525`, :issue:`9528`, :issue:`9536`, :issue:`9572`, :issue:`9576`, :issue:`9596`, :issue:`9683`, :issue:`9699`, :issue:`9772`, etc.) +- Add :attr:`DMChannel.recipients` to get all recipients of a DM channel (:issue:`9760`). +- Add support for :attr:`RawReactionActionEvent.message_author_id`. +- Add support for :attr:`AuditLogAction.creator_monetization_request_created` and :attr:`AuditLogAction.creator_monetization_terms_accepted`. +- Add support for :class:`AttachmentFlags`, accessed via :attr:`Attachment.flags` (:issue:`9486`). +- Add support for :class:`RoleFlags`, accessed via :attr:`Role.flags` (:issue:`9485`). +- Add support for :attr:`ChannelType.media`, accessed via :meth:`ForumChannel.is_media`. +- Add various new permissions (:issue:`9501`, :issue:`9762`, :issue:`9759`, :issue:`9857`) + - Add :meth:`Permissions.events`. + - Add :attr:`Permissions.create_events`. + - Add :attr:`Permissions.view_creator_monetization_analytics`. + - Add :attr:`Permissions.send_polls` + - Add :attr:`Permissions.create_polls`. + - Add :attr:`Permissions.use_external_apps`. + +- Add shortcut for :attr:`CategoryChannel.forums`. +- Add encoder options to :meth:`VoiceClient.play` (:issue:`9527`). +- Add support for team member roles. + - Add :class:`TeamMemberRole`. + - Add :attr:`TeamMember.role`. + - Updated :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` to account for team roles. Team owners or developers are considered Bot owners. + +- Add optional attribute ``integration_type`` in :attr:`AuditLogEntry.extra` for ``kick`` or ``member_role_update`` actions. +- Add support for "dynamic" :class:`ui.Item` that let you parse state out of a ``custom_id`` using regex. + - In order to use this, you must subclass :class:`ui.DynamicItem`. + - This is an alternative to persistent views. + - Add :meth:`Client.add_dynamic_items`. + - Add :meth:`Client.remove_dynamic_items`. + - Add :meth:`ui.Item.interaction_check`. + - Check the :resource:`dynamic_counter example ` for more information. + +- Add support for reading burst reactions. The API does not support sending them as of currently. + - Add :attr:`Reaction.normal_count`. + - Add :attr:`Reaction.burst_count`. + - Add :attr:`Reaction.me_burst`. + +- Add support for default values on select menus (:issue:`9577`). + - Add :class:`SelectDefaultValue`. + - Add :class:`SelectDefaultValueType`. + - Add a ``default_values`` attribute to each specialised select menu. + +- Add ``scheduled_event`` parameter for :meth:`StageChannel.create_instance` (:issue:`9595`). +- Add support for auto mod members (:issue:`9328`). + - Add ``type`` keyword argument to :class:`AutoModRuleAction`. + - Add :attr:`AutoModTrigger.mention_raid_protection`. + - Add :attr:`AutoModRuleTriggerType.member_profile`. + - Add :attr:`AutoModRuleEventType.member_update`. + - Add :attr:`AutoModRuleActionType.block_member_interactions`. + +- Add support for premium app integrations (:issue:`9453`). + - Add multiple SKU and entitlement related classes, e.g. :class:`SKU`, :class:`Entitlement`, :class:`SKUFlags`. + - Add multiple enums, e.g. :class:`SKUType`, :class:`EntitlementType`, :class:`EntitlementOwnerType`. + - Add :meth:`Client.fetch_skus` and :meth:`Client.fetch_entitlement` to fetch from the API. + - Add :meth:`Client.create_entitlement` to create entitlements. + - Add :attr:`Client.entitlements`. + - Add :attr:`Interaction.entitlement_sku_ids`. + - Add :attr:`Interaction.entitlements`. + - Add :attr:`ButtonStyle.premium` and :attr:`ui.Button.sku_id` to send a button asking the user to buy an SKU (:issue:`9845`). + - Add support for one time purchase (:issue:`9803`). + +- Add support for editing application info (:issue:`9610`). + - Add :attr:`AppInfo.interactions_endpoint_url`. + - Add :attr:`AppInfo.redirect_uris`. + - Add :meth:`AppInfo.edit`. + +- Add support for getting/fetching threads from :class:`Message` (:issue:`9665`). + - Add :attr:`PartialMessage.thread`. + - Add :attr:`Message.thread`. + - Add :meth:`Message.fetch_thread`. + +- Add support for platform and assets to activities (:issue:`9677`). + - Add :attr:`Activity.platform`. + - Add :attr:`Game.platform`. + - Add :attr:`Game.assets`. + +- Add support for suppressing embeds in an interaction response (:issue:`9678`). +- Add support for adding forum thread tags via webhook (:issue:`9680`) and (:issue:`9783`). +- Add support for guild incident message types (:issue:`9686`). +- Add :attr:`Locale.latin_american_spanish` (:issue:`9689`). +- Add support for setting voice channel status (:issue:`9603`). +- Add a shard connect timeout parameter to :class:`AutoShardedClient`. +- Add support for guild incidents (:issue:`9590`). + - Updated :meth:`Guild.edit` with ``invites_disabled_until`` and ``dms_disabled_until`` parameters. + - Add :attr:`Guild.invites_paused_until`. + - Add :attr:`Guild.dms_paused_until`. + - Add :meth:`Guild.invites_paused`. + - Add :meth:`Guild.dms_paused`. + +- Add support for :attr:`abc.User.avatar_decoration` (:issue:`9343`). +- Add support for GIF stickers (:issue:`9737`). +- Add support for updating :class:`ClientUser` banners (:issue:`9752`). +- Add support for bulk banning members via :meth:`Guild.bulk_ban`. +- Add ``reason`` keyword argument to :meth:`Thread.delete` (:issue:`9804`). +- Add :attr:`AppInfo.approximate_guild_count` (:issue:`9811`). +- Add support for :attr:`Message.interaction_metadata` (:issue:`9817`). +- Add support for differing :class:`Invite` types (:issue:`9682`). +- Add support for reaction types to raw and non-raw models (:issue:`9836`). +- |tasks| Add ``name`` parameter to :meth:`~ext.tasks.loop` to name the internal :class:`asyncio.Task`. +- |commands| Add fallback behaviour to :class:`~ext.commands.CurrentGuild`. +- |commands| Add logging for errors that occur during :meth:`~ext.commands.Cog.cog_unload`. +- |commands| Add support for :class:`typing.NewType` and ``type`` keyword type aliases (:issue:`9815`). + - Also supports application commands. + +- |commands| Add support for positional-only flag parameters (:issue:`9805`). +- |commands| Add support for channel URLs in ChannelConverter related classes (:issue:`9799`). + + +Bug Fixes +~~~~~~~~~~ + +- Fix emoji and sticker cache being populated despite turning the intent off. +- Fix outstanding chunk requests when receiving a gateway READY event not being cleared (:issue:`9571`). +- Fix escape behaviour for lists and headers in :meth:`~utils.escape_markdown`. +- Fix alias value for :attr:`Intents.auto_moderation` (:issue:`9524`). +- Fixes and improvements for :class:`FFmpegAudio` and all related subclasses (:issue:`9528`). +- Fix :meth:`Template.source_guild` attempting to resolve from cache (:issue:`9535`). +- Fix :exc:`IndexError` being raised instead of :exc:`ValueError` when calling :meth:`Colour.from_str` with an empty string (:issue:`9540`). +- Fix :meth:`View.from_message` not correctly creating the varying :class:`ui.Select` types (:issue:`9559`). +- Fix logging with autocomplete exceptions, which were previously suppressed. +- Fix possible error in voice cleanup logic (:issue:`9572`). +- Fix possible :exc:`AttributeError` during :meth:`app_commands.CommandTree.sync` when a command is regarded as 'too large'. +- Fix possible :exc:`TypeError` if a :class:`app_commands.Group` did not have a name set (:issue:`9581`). +- Fix possible bad voice state where you move to a voice channel with missing permissions (:issue:`9596`). +- Fix websocket reaching an error state due to received error payload (:issue:`9561`). +- Fix handling of :class:`AuditLogDiff` when relating to auto mod triggers (:issue:`9622`). +- Fix race condition in voice logic relating to disconnect and connect (:issue:`9683`). +- Use the :attr:`Interaction.user` guild as a fallback for :attr:`Interaction.guild` if not available. +- Fix restriction on auto moderation audit log ID range. +- Fix check for maximum number of children per :class:`ui.View`. +- Fix comparison between :class:`Object` classes with a ``type`` set. +- Fix handling of an enum in :meth:`AutoModRule.edit` (:issue:`9798`). +- Fix handling of :meth:`Client.close` within :meth:`Client.__aexit__` (:issue:`9769`). +- Fix channel deletion not evicting related threads from cache (:issue:`9796`). +- Fix bug with cache superfluously incrementing role positions (:issue:`9853`). +- Fix ``exempt_channels`` not being passed along in :meth:`Guild.create_automod_rule` (:issue:`9861`). +- Fix :meth:`abc.GuildChannel.purge` failing on single-message delete mode if the message was deleted (:issue:`9830`, :issue:`9863`). +- |commands| Fix localization support for :class:`~ext.commands.HybridGroup` fallback. +- |commands| Fix nested :class:`~ext.commands.HybridGroup`'s inserting manual app commands. +- |commands| Fix an issue where :class:`~ext.commands.HybridGroup` wrapped instances would be out of sync. +- |commands| Fix :class:`~ext.commands.HelpCommand` defined checks not carrying over during copy (:issue:`9843`). + +Miscellaneous +~~~~~~~~~~~~~~ + +- Additional documentation added for logging capabilities. +- Performance increases of constructing :class:`Permissions` using keyword arguments. +- Improve ``__repr__`` of :class:`SyncWebhook` and :class:`Webhook` (:issue:`9764`). +- Change internal thread names to be consistent (:issue:`9538`). + .. _vp2p3p2: v2.3.2 @@ -23,7 +386,6 @@ Bug Fixes - 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`. diff --git a/examples/views/layout.py b/examples/views/layout.py new file mode 100644 index 000000000000..70effc30cd31 --- /dev/null +++ b/examples/views/layout.py @@ -0,0 +1,47 @@ +# This example requires the 'message_content' privileged intent to function. + +from discord.ext import commands + +import discord + + +class Bot(commands.Bot): + def __init__(self): + intents = discord.Intents.default() + intents.message_content = True + + super().__init__(command_prefix=commands.when_mentioned_or('$'), intents=intents) + + async def on_ready(self): + print(f'Logged in as {self.user} (ID: {self.user.id})') + print('------') + + +# Define a LayoutView, which will allow us to add v2 components to it. +class Layout(discord.ui.LayoutView): + # you can add any top-level component (ui.ActionRow, ui.Section, ui.Container, ui.File, etc.) here + + action_row = discord.ui.ActionRow() + + @action_row.button(label='Click Me!') + async def action_row_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('Hi!', ephemeral=True) + + container = discord.ui.Container( + discord.ui.TextDisplay( + 'Click the above button to receive a **very special** message!', + ), + accent_colour=discord.Colour.blurple(), + ) + + +bot = Bot() + + +@bot.command() +async def layout(ctx: commands.Context): + """Sends a very special message!""" + await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any content, embed(s), stickers, or poll + + +bot.run('token') diff --git a/pyproject.toml b/pyproject.toml index 559b24b4ba55..92ccb738106d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,91 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" +[project] +name = "discord.py" +description = "A Python wrapper for the Discord API" +readme = { file = "README.rst", content-type = "text/x-rst" } +license = { file = "LICENSE" } +requires-python = ">=3.8" +authors = [{ name = "Rapptz" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "Typing :: Typed", +] +dynamic = ["version", "dependencies"] + +[project.urls] +Documentation = "https://discordpy.readthedocs.io/en/latest/" +"Issue tracker" = "https://github.com/Rapptz/discord.py/issues" + +[tool.setuptools.dynamic] +dependencies = { file = "requirements.txt" } + +[project.optional-dependencies] +voice = ["PyNaCl>=1.5.0,<1.6"] +docs = [ + "sphinx==4.4.0", + "sphinxcontrib_trio==1.1.2", + # TODO: bump these when migrating to a newer Sphinx version + "sphinxcontrib-websupport==1.2.4", + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-jsmath==1.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", + "typing-extensions>=4.3,<5", + "sphinx-inline-tabs==2023.4.21", + # TODO: Remove this when moving to Sphinx >= 6.6 + "imghdr-lts==1.0.0; python_version>='3.13'", +] +speed = [ + "orjson>=3.5.4", + "aiodns>=1.1; sys_platform != 'win32'", + "Brotli", + "cchardet==2.1.7; python_version < '3.10'", + "zstandard>=0.23.0" +] +test = [ + "coverage[toml]", + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-mock", + "typing-extensions>=4.3,<5", + "tzdata; sys_platform == 'win32'", +] +dev = [ + "black==22.6", + "typing_extensions>=4.3,<5", +] + +[tool.setuptools] +packages = [ + "discord", + "discord.types", + "discord.ui", + "discord.webhook", + "discord.app_commands", + "discord.ext.commands", + "discord.ext.tasks", +] +include-package-data = true + [tool.black] line-length = 125 skip-string-normalization = true @@ -16,7 +100,7 @@ omit = [ [tool.coverage.report] exclude_lines = [ "pragma: no cover", - "@overload" + "@overload", ] [tool.isort] diff --git a/requirements.txt b/requirements.txt index 74dedab377f8..ef2b6c534f42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ aiohttp>=3.7.4,<4 -async-timeout>=4.0,<5.0; python_version<"3.11" +audioop-lts; python_version>='3.13' diff --git a/setup.py b/setup.py index 1b0fbdcfea1d..e3d6d59f4fff 100644 --- a/setup.py +++ b/setup.py @@ -1,105 +1,32 @@ from setuptools import setup import re -requirements = [] -with open('requirements.txt') as f: - requirements = f.read().splitlines() -version = '' -with open('discord/__init__.py') as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) +def derive_version() -> str: + version = '' + with open('discord/__init__.py') as f: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) -if not version: - raise RuntimeError('version is not set') + if not version: + raise RuntimeError('version is not set') -if version.endswith(('a', 'b', 'rc')): - # append version identifier based on commit count - try: - import subprocess + if version.endswith(('a', 'b', 'rc')): + # append version identifier based on commit count + try: + import subprocess - p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - if out: - version += out.decode('utf-8').strip() - p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - if out: - version += '+g' + out.decode('utf-8').strip() - except Exception: - pass + p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += out.decode('utf-8').strip() + p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += '+g' + out.decode('utf-8').strip() + except Exception: + pass -readme = '' -with open('README.rst') as f: - readme = f.read() + return version -extras_require = { - 'voice': ['PyNaCl>=1.3.0,<1.6'], - 'docs': [ - 'sphinx==4.4.0', - 'sphinxcontrib_trio==1.1.2', - 'sphinxcontrib-websupport', - 'typing-extensions>=4.3,<5', - 'sphinx-inline-tabs', - ], - 'speed': [ - 'orjson>=3.5.4', - 'aiodns>=1.1', - 'Brotli', - 'cchardet==2.1.7; python_version < "3.10"', - ], - 'test': [ - 'coverage[toml]', - 'pytest', - 'pytest-asyncio', - 'pytest-cov', - 'pytest-mock', - 'typing-extensions>=4.3,<5', - 'tzdata; sys_platform == "win32"', - ], -} -packages = [ - 'discord', - 'discord.types', - 'discord.ui', - 'discord.webhook', - 'discord.app_commands', - 'discord.ext.commands', - 'discord.ext.tasks', -] - -setup( - name='discord.py', - author='Rapptz', - url='https://github.com/Rapptz/discord.py', - project_urls={ - 'Documentation': 'https://discordpy.readthedocs.io/en/latest/', - 'Issue tracker': 'https://github.com/Rapptz/discord.py/issues', - }, - version=version, - packages=packages, - license='MIT', - description='A Python wrapper for the Discord API', - long_description=readme, - long_description_content_type='text/x-rst', - include_package_data=True, - install_requires=requirements, - extras_require=extras_require, - python_requires='>=3.8.0', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - 'Typing :: Typed', - ], -) +setup(version=derive_version()) diff --git a/tests/test_colour.py b/tests/test_colour.py index bf0e597133a6..b79f153f06e8 100644 --- a/tests/test_colour.py +++ b/tests/test_colour.py @@ -44,6 +44,7 @@ ('rgb(20%, 24%, 56%)', 0x333D8F), ('rgb(20%, 23.9%, 56.1%)', 0x333D8F), ('rgb(51, 61, 143)', 0x333D8F), + ('0x#333D8F', 0x333D8F), ], ) def test_from_str(value, expected): @@ -53,6 +54,7 @@ def test_from_str(value, expected): @pytest.mark.parametrize( ('value'), [ + None, 'not valid', '0xYEAH', '#YEAH', @@ -62,8 +64,72 @@ def test_from_str(value, expected): 'rgb(30, -1, 60)', 'invalid(a, b, c)', 'rgb(', + '#1000000', + '#FFFFFFF', + "rgb(101%, 50%, 50%)", + "rgb(50%, -10%, 50%)", + "rgb(50%, 50%, 150%)", + "rgb(256, 100, 100)", ], ) def test_from_str_failures(value): with pytest.raises(ValueError): discord.Colour.from_str(value) + + +@pytest.mark.parametrize( + ('value', 'expected'), + [ + (discord.Colour.default(), 0x000000), + (discord.Colour.teal(), 0x1ABC9C), + (discord.Colour.dark_teal(), 0x11806A), + (discord.Colour.brand_green(), 0x57F287), + (discord.Colour.green(), 0x2ECC71), + (discord.Colour.dark_green(), 0x1F8B4C), + (discord.Colour.blue(), 0x3498DB), + (discord.Colour.dark_blue(), 0x206694), + (discord.Colour.purple(), 0x9B59B6), + (discord.Colour.dark_purple(), 0x71368A), + (discord.Colour.magenta(), 0xE91E63), + (discord.Colour.dark_magenta(), 0xAD1457), + (discord.Colour.gold(), 0xF1C40F), + (discord.Colour.dark_gold(), 0xC27C0E), + (discord.Colour.orange(), 0xE67E22), + (discord.Colour.dark_orange(), 0xA84300), + (discord.Colour.brand_red(), 0xED4245), + (discord.Colour.red(), 0xE74C3C), + (discord.Colour.dark_red(), 0x992D22), + (discord.Colour.lighter_grey(), 0x95A5A6), + (discord.Colour.dark_grey(), 0x607D8B), + (discord.Colour.light_grey(), 0x979C9F), + (discord.Colour.darker_grey(), 0x546E7A), + (discord.Colour.og_blurple(), 0x7289DA), + (discord.Colour.blurple(), 0x5865F2), + (discord.Colour.greyple(), 0x99AAB5), + (discord.Colour.dark_theme(), 0x313338), + (discord.Colour.fuchsia(), 0xEB459E), + (discord.Colour.yellow(), 0xFEE75C), + (discord.Colour.dark_embed(), 0x2B2D31), + (discord.Colour.light_embed(), 0xEEEFF1), + (discord.Colour.pink(), 0xEB459F), + ], +) +def test_static_colours(value, expected): + assert value.value == expected + + + + +@pytest.mark.parametrize( + ('value', 'property', 'expected'), + [ + (discord.Colour(0x000000), 'r', 0), + (discord.Colour(0xFFFFFF), 'g', 255), + (discord.Colour(0xABCDEF), 'b', 239), + (discord.Colour(0x44243B), 'r', 68), + (discord.Colour(0x333D8F), 'g', 61), + (discord.Colour(0xDBFF00), 'b', 0), + ], +) +def test_colour_properties(value, property, expected): + assert getattr(value, property) == expected diff --git a/tests/test_embed.py b/tests/test_embed.py new file mode 100644 index 000000000000..3efedd6a57be --- /dev/null +++ b/tests/test_embed.py @@ -0,0 +1,269 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import datetime + +import discord +import pytest + + +@pytest.mark.parametrize( + ('title', 'description', 'colour', 'url'), + [ + ('title', 'description', 0xABCDEF, 'https://example.com'), + ('title', 'description', 0xFF1294, None), + ('title', 'description', discord.Colour(0x333D8F), 'https://example.com'), + ('title', 'description', discord.Colour(0x44243B), None), + ], +) +def test_embed_initialization(title, description, colour, url): + embed = discord.Embed(title=title, description=description, colour=colour, url=url) + assert embed.title == title + assert embed.description == description + assert embed.colour == colour or embed.colour == discord.Colour(colour) + assert embed.url == url + + +@pytest.mark.parametrize( + ('text', 'icon_url'), + [ + ('Hello discord.py', 'https://example.com'), + ('text', None), + (None, 'https://example.com'), + (None, None), + ], +) +def test_embed_set_footer(text, icon_url): + embed = discord.Embed() + embed.set_footer(text=text, icon_url=icon_url) + assert embed.footer.text == text + assert embed.footer.icon_url == icon_url + + +def test_embed_remove_footer(): + embed = discord.Embed() + embed.set_footer(text='Hello discord.py', icon_url='https://example.com') + embed.remove_footer() + assert embed.footer.text is None + assert embed.footer.icon_url is None + + +@pytest.mark.parametrize( + ('name', 'url', 'icon_url'), + [ + ('Rapptz', 'http://example.com', 'http://example.com/icon.png'), + ('NCPlayz', None, 'http://example.com/icon.png'), + ('Jackenmen', 'http://example.com', None), + ], +) +def test_embed_set_author(name, url, icon_url): + embed = discord.Embed() + embed.set_author(name=name, url=url, icon_url=icon_url) + assert embed.author.name == name + assert embed.author.url == url + assert embed.author.icon_url == icon_url + + +def test_embed_remove_author(): + embed = discord.Embed() + embed.set_author(name='Rapptz', url='http://example.com', icon_url='http://example.com/icon.png') + embed.remove_author() + assert embed.author.name is None + assert embed.author.url is None + assert embed.author.icon_url is None + + +@pytest.mark.parametrize( + ('thumbnail'), + [ + ('http://example.com'), + (None), + ], +) +def test_embed_set_thumbnail(thumbnail): + embed = discord.Embed() + embed.set_thumbnail(url=thumbnail) + assert embed.thumbnail.url == thumbnail + + +@pytest.mark.parametrize( + ('image'), + [ + ('http://example.com'), + (None), + ], +) +def test_embed_set_image(image): + embed = discord.Embed() + embed.set_image(url=image) + assert embed.image.url == image + + +@pytest.mark.parametrize( + ('name', 'value', 'inline'), + [ + ('music', 'music value', True), + ('sport', 'sport value', False), + ], +) +def test_embed_add_field(name, value, inline): + embed = discord.Embed() + embed.add_field(name=name, value=value, inline=inline) + assert len(embed.fields) == 1 + assert embed.fields[0].name == name + assert embed.fields[0].value == value + assert embed.fields[0].inline == inline + + +def test_embed_insert_field(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.insert_field_at(0, name='name 2', value='value 2', inline=False) + assert embed.fields[0].name == 'name 2' + assert embed.fields[0].value == 'value 2' + assert embed.fields[0].inline is False + + +def test_embed_set_field_at(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.set_field_at(0, name='name 2', value='value 2', inline=False) + assert embed.fields[0].name == 'name 2' + assert embed.fields[0].value == 'value 2' + assert embed.fields[0].inline is False + + +def test_embed_set_field_at_failure(): + embed = discord.Embed() + with pytest.raises(IndexError): + embed.set_field_at(0, name='name', value='value', inline=True) + + +def test_embed_clear_fields(): + embed = discord.Embed() + embed.add_field(name="field 1", value="value 1", inline=False) + embed.add_field(name="field 2", value="value 2", inline=False) + embed.add_field(name="field 3", value="value 3", inline=False) + embed.clear_fields() + assert len(embed.fields) == 0 + + +def test_embed_remove_field(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.remove_field(0) + assert len(embed.fields) == 0 + + +@pytest.mark.parametrize( + ('title', 'description', 'url'), + [ + ('title 1', 'description 1', 'https://example.com'), + ('title 2', 'description 2', None), + ], +) +def test_embed_copy(title, description, url): + embed = discord.Embed(title=title, description=description, url=url) + embed_copy = embed.copy() + + assert embed == embed_copy + assert embed.title == embed_copy.title + assert embed.description == embed_copy.description + assert embed.url == embed_copy.url + + +@pytest.mark.parametrize( + ('title', 'description'), + [ + ('title 1', 'description 1'), + ('title 2', 'description 2'), + ], +) +def test_embed_len(title, description): + embed = discord.Embed(title=title, description=description) + assert len(embed) == len(title) + len(description) + + +@pytest.mark.parametrize( + ('title', 'description', 'fields', 'footer', 'author'), + [ + ( + 'title 1', + 'description 1', + [('field name 1', 'field value 1'), ('field name 2', 'field value 2')], + 'footer 1', + 'author 1', + ), + ('title 2', 'description 2', [('field name 3', 'field value 3')], 'footer 2', 'author 2'), + ], +) +def test_embed_len_with_options(title, description, fields, footer, author): + embed = discord.Embed(title=title, description=description) + for name, value in fields: + embed.add_field(name=name, value=value) + embed.set_footer(text=footer) + embed.set_author(name=author) + assert len(embed) == len(title) + len(description) + len("".join([name + value for name, value in fields])) + len( + footer + ) + len(author) + + +def test_embed_to_dict(): + timestamp = datetime.datetime.now(datetime.timezone.utc) + embed = discord.Embed(title="Test Title", description="Test Description", timestamp=timestamp) + data = embed.to_dict() + assert data['title'] == "Test Title" + assert data['description'] == "Test Description" + assert data['timestamp'] == timestamp.isoformat() + + +def test_embed_from_dict(): + data = { + 'title': 'Test Title', + 'description': 'Test Description', + 'url': 'http://example.com', + 'color': 0x00FF00, + 'timestamp': '2024-07-03T12:34:56+00:00', + } + embed = discord.Embed.from_dict(data) + assert embed.title == 'Test Title' + assert embed.description == 'Test Description' + assert embed.url == 'http://example.com' + assert embed.colour is not None and embed.colour.value == 0x00FF00 + assert embed.timestamp is not None and embed.timestamp.isoformat() == '2024-07-03T12:34:56+00:00' + + +@pytest.mark.parametrize( + ('value'), + [ + -0.5, + '#FFFFFF', + ], +) +def test_embed_colour_setter_failure(value): + embed = discord.Embed() + with pytest.raises(TypeError): + embed.colour = value diff --git a/tests/test_files.py b/tests/test_files.py index 6096c3a3891b..72ff3b7b37cf 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -27,6 +27,7 @@ from io import BytesIO import discord +import pytest FILE = BytesIO() @@ -127,3 +128,58 @@ def test_file_not_spoiler_with_overriding_name_double_spoiler(): f.filename = 'SPOILER_SPOILER_.gitignore' assert f.filename == 'SPOILER_.gitignore' assert f.spoiler == True + + +def test_file_reset(): + f = discord.File('.gitignore') + + f.reset(seek=True) + assert f.fp.tell() == 0 + + f.reset(seek=False) + assert f.fp.tell() == 0 + + +def test_io_reset(): + f = discord.File(FILE) + + f.reset(seek=True) + assert f.fp.tell() == 0 + + f.reset(seek=False) + assert f.fp.tell() == 0 + + +def test_io_failure(): + class NonSeekableReadable(BytesIO): + def seekable(self): + return False + + def readable(self): + return False + + f = NonSeekableReadable() + + with pytest.raises(ValueError) as excinfo: + discord.File(f) + + assert str(excinfo.value) == f"File buffer {f!r} must be seekable and readable" + + +def test_io_to_dict(): + buffer = BytesIO(b"test content") + file = discord.File(buffer, filename="test.txt", description="test description") + + data = file.to_dict(0) + assert data["id"] == 0 + assert data["filename"] == "test.txt" + assert data["description"] == "test description" + + +def test_file_to_dict(): + f = discord.File('.gitignore', description="test description") + + data = f.to_dict(0) + assert data["id"] == 0 + assert data["filename"] == ".gitignore" + assert data["description"] == "test description" diff --git a/tests/test_permissions_all.py b/tests/test_permissions_all.py new file mode 100644 index 000000000000..883dc1b630ba --- /dev/null +++ b/tests/test_permissions_all.py @@ -0,0 +1,7 @@ +import discord + +from functools import reduce +from operator import or_ + +def test_permissions_all(): + assert discord.Permissions.all().value == reduce(or_, discord.Permissions.VALID_FLAGS.values()) diff --git a/tests/test_ui_buttons.py b/tests/test_ui_buttons.py new file mode 100644 index 000000000000..55c0c7cd8269 --- /dev/null +++ b/tests/test_ui_buttons.py @@ -0,0 +1,167 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +def test_button_init(): + button = discord.ui.Button( + label="Click me!", + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.secondary + assert button.disabled == False + assert button.url == None + assert button.emoji == None + assert button.sku_id == None + + +def test_button_with_sku_id(): + button = discord.ui.Button( + label="Click me!", + sku_id=1234567890, + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.premium + assert button.sku_id == 1234567890 + + +def test_button_with_url(): + button = discord.ui.Button( + label="Click me!", + url="https://example.com", + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.link + assert button.url == "https://example.com" + + +def test_mix_both_custom_id_and_url(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + url="https://example.com", + custom_id="test", + ) + + +def test_mix_both_custom_id_and_sku_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + sku_id=1234567890, + custom_id="test", + ) + + +def test_mix_both_url_and_sku_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + url="https://example.com", + sku_id=1234567890, + ) + + +def test_invalid_url(): + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.url = 1234567890 # type: ignore + + +def test_invalid_custom_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + custom_id=1234567890, # type: ignore + ) + + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.custom_id = 1234567890 # type: ignore + + +def test_button_with_partial_emoji(): + button = discord.ui.Button( + label="Click me!", + emoji="👍", + ) + assert button.label == "Click me!" + assert button.emoji is not None and button.emoji.name == "👍" + + +def test_button_with_str_emoji(): + emoji = discord.PartialEmoji(name="👍") + button = discord.ui.Button( + label="Click me!", + emoji=emoji, + ) + assert button.label == "Click me!" + assert button.emoji == emoji + + +def test_button_with_invalid_emoji(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + emoji=-0.53, # type: ignore + ) + + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.emoji = -0.53 # type: ignore + + +def test_button_setter(): + button = discord.ui.Button() + + button.label = "Click me!" + assert button.label == "Click me!" + + button.style = discord.ButtonStyle.primary + assert button.style == discord.ButtonStyle.primary + + button.disabled = True + assert button.disabled == True + + button.url = "https://example.com" + assert button.url == "https://example.com" + + button.emoji = "👍" + assert button.emoji is not None and button.emoji.name == "👍" # type: ignore + + button.custom_id = "test" + assert button.custom_id == "test" + + button.sku_id = 1234567890 + assert button.sku_id == 1234567890 diff --git a/tests/test_ui_modals.py b/tests/test_ui_modals.py new file mode 100644 index 000000000000..dd1ac7169187 --- /dev/null +++ b/tests/test_ui_modals.py @@ -0,0 +1,102 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +@pytest.mark.asyncio +async def test_modal_init(): + modal = discord.ui.Modal( + title="Temp Title", + ) + assert modal.title == "Temp Title" + assert modal.timeout == None + + +@pytest.mark.asyncio +async def test_no_title(): + with pytest.raises(ValueError) as excinfo: + discord.ui.Modal() + + assert str(excinfo.value) == "Modal must have a title" + + +@pytest.mark.asyncio +async def test_to_dict(): + modal = discord.ui.Modal( + title="Temp Title", + ) + data = modal.to_dict() + assert data["custom_id"] is not None + assert data["title"] == "Temp Title" + assert data["components"] == [] + + +@pytest.mark.asyncio +async def test_add_item(): + modal = discord.ui.Modal( + title="Temp Title", + ) + item = discord.ui.TextInput(label="Test") + modal.add_item(item) + + assert modal.children == [item] + + +@pytest.mark.asyncio +async def test_add_item_invalid(): + modal = discord.ui.Modal( + title="Temp Title", + ) + with pytest.raises(TypeError): + modal.add_item("Not an item") # type: ignore + + +@pytest.mark.asyncio +async def test_maximum_items(): + modal = discord.ui.Modal( + title="Temp Title", + ) + max_item_limit = 5 + + for i in range(max_item_limit): + modal.add_item(discord.ui.TextInput(label=f"Test {i}")) + + with pytest.raises(ValueError): + modal.add_item(discord.ui.TextInput(label="Test")) + + +@pytest.mark.asyncio +async def test_modal_setters(): + modal = discord.ui.Modal( + title="Temp Title", + ) + modal.title = "New Title" + assert modal.title == "New Title" + + modal.timeout = 120 + assert modal.timeout == 120 diff --git a/tests/test_ui_selects.py b/tests/test_ui_selects.py new file mode 100644 index 000000000000..a9019c3de995 --- /dev/null +++ b/tests/test_ui_selects.py @@ -0,0 +1,39 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +@pytest.mark.asyncio +async def test_add_option(): + select = discord.ui.Select() + + for i in range(1, 25 + 1): + select.add_option(label=str(i), value=str(i)) + + with pytest.raises(ValueError): + select.add_option(label="26", value="26")