diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f05f7d35f..05c5bdd33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,25 +47,13 @@ jobs: shell: bash run: | cd docs + sphinx-build -b html -D language=en -a -n -T -W --keep-going . _build/html + env: + DOCS_LANGUAGE: en - EXIT_STATUS=0 - # Build English docs - sphinx-build -b html -D language=en -a -n -T -W --keep-going . _build_en || EXIT_STATUS=$? - # Build Japanese docs - sphinx-build -b html -D language=ja -a -n -T -W --keep-going . _build_ja || EXIT_STATUS=$? - - exit ${EXIT_STATUS} - - # - name: Upload EN docs + # - name: Upload docs # uses: actions/upload-artifact@v2 # if: always() # with: # name: docs-en - # path: docs/_build_en/* - - # - name: Upload JA docs - # uses: actions/upload-artifact@v2 - # if: always() - # with: - # name: docs-ja - # path: docs/_build_ja/* + # path: docs/_build/html/* diff --git a/.github/workflows/scripts/close_and_reopen_pr.js b/.github/workflows/scripts/close_and_reopen_pr.js new file mode 100644 index 000000000..282dc3ca2 --- /dev/null +++ b/.github/workflows/scripts/close_and_reopen_pr.js @@ -0,0 +1,28 @@ +module.exports = (async function ({github, context}) { + const pr_number = process.env.PR_NUMBER; + const pr_operation = process.env.PR_OPERATION; + + if (!['created', 'updated'].includes(pr_operation)) { + console.log('PR was not created as there were no changes.') + return; + } + + // Close the PR + github.issues.update({ + issue_number: pr_number, + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed' + }); + + // Wait a moment for GitHub to process it... + await new Promise(r => setTimeout(r, 2000)); + + // Then reopen the PR so it runs CI + github.issues.update({ + issue_number: pr_number, + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open' + }); +}) diff --git a/discord/abc.py b/discord/abc.py index f05e271ef..55eb47c4e 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -93,8 +93,7 @@ if TYPE_CHECKING: SnowflakeList, ) - PartialMessageableChannel = Union[TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable] - MessageableChannel = Union[PartialMessageableChannel, GroupChannel] + MessageableChannel = Union[TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] MISSING = utils.MISSING diff --git a/discord/channel.py b/discord/channel.py index 7fc63bf1c..de45989e9 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -34,6 +34,7 @@ from typing import ( Mapping, Optional, TYPE_CHECKING, + Sequence, Tuple, Union, overload, @@ -53,6 +54,7 @@ from .errors import ClientException from .stage_instance import StageInstance from .threads import Thread from .invite import Invite +from .http import handle_message_parameters __all__ = ( 'TextChannel', @@ -60,6 +62,7 @@ __all__ = ( 'StageChannel', 'DMChannel', 'CategoryChannel', + 'ForumChannel', 'GroupChannel', 'PartialMessageable', ) @@ -73,8 +76,11 @@ if TYPE_CHECKING: from .member import Member, VoiceState from .abc import Snowflake, SnowflakeTime, T as ConnectReturn from .message import Message, PartialMessage + from .mentions import AllowedMentions from .webhook import Webhook from .state import ConnectionState + from .sticker import GuildSticker, StickerItem + from .file import File from .user import ClientUser, User, BaseUser from .guild import Guild, GuildChannel as GuildChannelType from .types.channel import ( @@ -84,8 +90,11 @@ if TYPE_CHECKING: DMChannel as DMChannelPayload, CategoryChannel as CategoryChannelPayload, GroupDMChannel as GroupChannelPayload, + ForumChannel as ForumChannelPayload, ) + from .types.snowflake import SnowflakeList + class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. @@ -231,7 +240,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): @property def last_message(self) -> Optional[Message]: - """Fetches the last message from this channel in cache. + """Retrieves the last message from this channel in cache. The message might not be valid or point to an existing message. @@ -1008,7 +1017,7 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): @property def last_message(self) -> Optional[Message]: - """Fetches the last message from this channel in cache. + """Retrieves the last message from this channel in cache. The message might not be valid or point to an existing message. @@ -1848,6 +1857,338 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): return await self.guild.create_stage_channel(name, category=self, **options) +class ForumChannel(discord.abc.GuildChannel, Hashable): + """Represents a Discord guild forum channel. + + .. versionadded:: 2.0 + + .. container:: operations + + .. describe:: x == y + + Checks if two forums are equal. + + .. describe:: x != y + + Checks if two forums are not equal. + + .. describe:: hash(x) + + Returns the forum's hash. + + .. describe:: str(x) + + Returns the forum's name. + + Attributes + ----------- + name: :class:`str` + The forum name. + guild: :class:`Guild` + The guild the forum belongs to. + id: :class:`int` + The forum ID. + category_id: Optional[:class:`int`] + The category channel ID this forum belongs to, if applicable. + topic: Optional[:class:`str`] + The forum's topic. ``None`` if it doesn't exist. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + last_message_id: Optional[:class:`int`] + The last thread ID that was created on this forum. This technically also + coincides with the message ID that started the thread that was created. + It may *not* point to an existing or valid thread or message. + slowmode_delay: :class:`int` + The number of seconds a member must wait between creating threads + in this forum. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + nsfw: :class:`bool` + If the forum is marked as "not safe for work" or "age restricted". + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this forum. + """ + + __slots__ = ( + 'name', + 'id', + 'guild', + 'topic', + '_state', + '_flags', + 'nsfw', + 'category_id', + 'position', + 'slowmode_delay', + '_overwrites', + 'last_message_id', + 'default_auto_archive_duration', + ) + + def __init__(self, *, state: ConnectionState, guild: Guild, data: ForumChannelPayload): + self._state: ConnectionState = state + self.id: int = int(data['id']) + self._update(guild, data) + + def __repr__(self) -> str: + attrs = [ + ('id', self.id), + ('name', self.name), + ('position', self.position), + ('nsfw', self.nsfw), + ('category_id', self.category_id), + ] + joined = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {joined}>' + + def _update(self, guild: Guild, data: ForumChannelPayload) -> None: + self.guild: Guild = guild + self.name: str = data['name'] + self.category_id: Optional[int] = utils._get_as_snowflake(data, 'parent_id') + self.topic: Optional[str] = data.get('topic') + self.position: int = data['position'] + self.nsfw: bool = data.get('nsfw', False) + self.slowmode_delay: int = data.get('rate_limit_per_user', 0) + self.default_auto_archive_duration: ThreadArchiveDuration = data.get('default_auto_archive_duration', 1440) + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self._fill_overwrites(data) + + @property + def type(self) -> ChannelType: + """:class:`ChannelType`: The channel's Discord type.""" + return ChannelType.forum + + @property + def _sorting_bucket(self) -> int: + return ChannelType.text.value + + @utils.copy_doc(discord.abc.GuildChannel.permissions_for) + def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: + base = super().permissions_for(obj) + + # text channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + return base + + @property + def threads(self) -> List[Thread]: + """List[:class:`Thread`]: Returns all the threads that you can see.""" + return [thread for thread in self.guild._threads.values() if thread.parent_id == self.id] + + def is_nsfw(self) -> bool: + """:class:`bool`: Checks if the forum is NSFW.""" + return self.nsfw + + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> ForumChannel: + return await self._clone_impl( + {'topic': self.topic, 'nsfw': self.nsfw, 'rate_limit_per_user': self.slowmode_delay}, name=name, reason=reason + ) + + @overload + async def edit( + self, + *, + reason: Optional[str] = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[CategoryChannel] = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + type: ChannelType = ..., + overwrites: Mapping[Union[Role, Member, Snowflake], PermissionOverwrite] = ..., + ) -> Optional[ForumChannel]: + ... + + @overload + async def edit(self) -> Optional[ForumChannel]: + ... + + async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[ForumChannel]: + """|coro| + + Edits the forum. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + Parameters + ---------- + name: :class:`str` + The new forum name. + topic: :class:`str` + The new forum's topic. + position: :class:`int` + The new forum's position. + nsfw: :class:`bool` + To mark the forum as NSFW or not. + sync_permissions: :class:`bool` + Whether to sync permissions with the forum's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this forum. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this forum, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + type: :class:`ChannelType` + Change the type of this text forum. Currently, only conversion between + :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This + is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. + reason: Optional[:class:`str`] + The reason for editing this forum. Shows up on the audit log. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the forum. + default_auto_archive_duration: :class:`int` + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + + Raises + ------ + ValueError + The new ``position`` is less than 0 or greater than the number of channels. + TypeError + The permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the forum. + HTTPException + Editing the forum failed. + + Returns + -------- + Optional[:class:`.ForumChannel`] + The newly edited forum channel. If the edit was only positional + then ``None`` is returned instead. + """ + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def create_thread( + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = MISSING, + slowmode_delay: Optional[int] = None, + content: Optional[str] = None, + tts: bool = False, + file: File = MISSING, + files: Sequence[File] = MISSING, + stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + mention_author: bool = MISSING, + suppress_embeds: bool = False, + reason: Optional[str] = None, + ) -> Thread: + """|coro| + + Creates a thread in this forum. + + This thread is a public thread with the initial message given. Currently in order + to start a thread in this forum, the user needs :attr:`~discord.Permissions.send_messages`. + + Parameters + ----------- + name: :class:`str` + The name of the thread. + auto_archive_duration: :class:`int` + The duration in minutes before a thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for user in this channel, in seconds. + The maximum value possible is `21600`. By default no slowmode rate limit + if this is ``None``. + content: Optional[:class:`str`] + The content of the message to send with the thread. + tts: :class:`bool` + Indicates if the message should be sent using text-to-speech. + file: :class:`~discord.File` + The file to upload. + files: List[:class:`~discord.File`] + A list of files to upload. Must be a maximum of 10. + allowed_mentions: :class:`~discord.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions` + are used instead. + mention_author: :class:`bool` + If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. + 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` + Whether to suppress embeds for the message. This sends the message without any embeds if set to ``True``. + reason: :class:`str` + The reason for creating a new thread. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to create a thread. + HTTPException + Starting the thread failed. + ValueError + The ``files`` or ``embeds`` list is not of the appropriate size. + TypeError + You specified both ``file`` and ``files``, + or you specified both ``embed`` and ``embeds``. + + Returns + -------- + :class:`Thread` + The created thread + """ + + state = self._state + previous_allowed_mention = state.allowed_mentions + if stickers is MISSING: + sticker_ids = MISSING + else: + sticker_ids: SnowflakeList = [s.id for s in stickers] + + if suppress_embeds: + from .message import MessageFlags # circular import + + flags = MessageFlags._from_value(4) + else: + flags = MISSING + + content = str(content) if content else MISSING + + extras = { + 'name': name, + 'auto_archive_duration': auto_archive_duration or self.default_auto_archive_duration, + 'location': 'Forum Channel', + 'type': 11, # Private threads don't seem to be allowed + } + if slowmode_delay is not None: + extras['rate_limit_per_user'] = slowmode_delay + + with handle_message_parameters( + content=content, + tts=tts, + file=file, + files=files, + allowed_mentions=allowed_mentions, + previous_allowed_mentions=previous_allowed_mention, + mention_author=None if mention_author is MISSING else mention_author, + stickers=sticker_ids, + flags=flags, + extras=extras, + ) as params: + data = await state.http.start_thread_in_forum(self.id, params=params, reason=reason) + return Thread(guild=self.guild, state=self._state, data=data) + + class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): """Represents a Discord direct message channel. @@ -2467,6 +2808,8 @@ def _guild_channel_factory(channel_type: int): return TextChannel, value elif value is ChannelType.stage_voice: return StageChannel, value + elif value is ChannelType.forum: + return ForumChannel, value else: return None, value diff --git a/discord/embeds.py b/discord/embeds.py index e28b0c59e..630f3d8c2 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -117,6 +117,7 @@ class Embed: title: Optional[:class:`str`] The title of the embed. This can be set during initialisation. + Can only be up to 256 characters. type: :class:`str` The type of embed. Usually "rich". This can be set during initialisation. @@ -125,6 +126,7 @@ class Embed: description: Optional[:class:`str`] The description of the embed. This can be set during initialisation. + Can only be up to 4096 characters. url: Optional[:class:`str`] The URL of the embed. This can be set during initialisation. @@ -335,7 +337,7 @@ class Embed: Parameters ----------- text: :class:`str` - The footer text. + The footer text. Can only be up to 2048 characters. icon_url: :class:`str` The URL of the footer icon. Only HTTP(S) is supported. """ @@ -493,7 +495,7 @@ class Embed: Parameters ----------- name: :class:`str` - The name of the author. + The name of the author. Can only be up to 256 characters. url: :class:`str` The URL for the author. icon_url: :class:`str` @@ -542,14 +544,14 @@ class Embed: """Adds a field to the embed object. This function returns the class instance to allow for fluent-style - chaining. + chaining. Can only be up to 25 fields. Parameters ----------- name: :class:`str` - The name of the field. + The name of the field. Can only be up to 256 characters. value: :class:`str` - The value of the field. + The value of the field. Can only be up to 1024 characters. inline: :class:`bool` Whether the field should be displayed inline. """ @@ -571,7 +573,7 @@ class Embed: """Inserts a field before a specified index to the embed. This function returns the class instance to allow for fluent-style - chaining. + chaining. Can only be up to 25 fields. .. versionadded:: 1.2 @@ -580,9 +582,9 @@ class Embed: index: :class:`int` The index of where to insert the field. name: :class:`str` - The name of the field. + The name of the field. Can only be up to 256 characters. value: :class:`str` - The value of the field. + The value of the field. Can only be up to 1024 characters. inline: :class:`bool` Whether the field should be displayed inline. """ @@ -631,7 +633,7 @@ class Embed: def set_field_at(self, index: int, *, name: Any, value: Any, inline: bool = True) -> Self: """Modifies a field to the embed object. - The index must point to a valid pre-existing field. + The index must point to a valid pre-existing field. Can only be up to 25 fields. This function returns the class instance to allow for fluent-style chaining. @@ -641,9 +643,9 @@ class Embed: index: :class:`int` The index of the field to modify. name: :class:`str` - The name of the field. + The name of the field. Can only be up to 256 characters. value: :class:`str` - The value of the field. + The value of the field. Can only be up to 1024 characters. inline: :class:`bool` Whether the field should be displayed inline. diff --git a/discord/enums.py b/discord/enums.py index 195252306..e978b1756 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -213,6 +213,7 @@ class ChannelType(Enum): public_thread = 11 private_thread = 12 stage_voice = 13 + forum = 15 def __str__(self) -> str: return self.name diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py index 3da57d80b..61d66090e 100644 --- a/discord/ext/commands/__init__.py +++ b/discord/ext/commands/__init__.py @@ -9,11 +9,12 @@ An extension module to facilitate creation of bot commands. """ from .bot import * +from .cog import * from .context import * -from .core import * -from .errors import * -from .help import * from .converter import * from .cooldowns import * -from .cog import * +from .core import * +from .errors import * from .flags import * +from .help import * +from .parameters import * diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 55d24f7a4..2146a2213 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -1266,8 +1266,7 @@ class Bot(BotBase, discord.Client): :meth:`.is_owner` then it will error. owner_ids: Optional[Collection[:class:`int`]] The user IDs that owns the bot. This is similar to :attr:`owner_id`. - If this is not set and the application is team based, then it is - fetched automatically using :meth:`~.Bot.application_info`. + If this is not set and and is then queried via :meth:`.is_owner` then it will error. For performance reasons it is recommended to use a :class:`set` for the collection. You cannot set both ``owner_id`` and ``owner_ids``. diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 55108d9d7..d5269ee84 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -23,18 +23,15 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -import inspect import re - -from typing import Any, Dict, Generic, List, Optional, TYPE_CHECKING, TypeVar, Union - -from ._types import BotT +from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union import discord.abc import discord.utils - from discord.message import Message +from ._types import BotT + if TYPE_CHECKING: from typing_extensions import ParamSpec @@ -47,6 +44,7 @@ if TYPE_CHECKING: from .cog import Cog from .core import Command + from .parameters import Parameter from .view import StringView # fmt: off @@ -90,7 +88,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): A dictionary of transformed arguments that were passed into the command. Similar to :attr:`args`\, if this is accessed in the :func:`.on_command_error` event then this dict could be incomplete. - current_parameter: Optional[:class:`inspect.Parameter`] + current_parameter: Optional[:class:`Parameter`] The parameter that is currently being inspected and converted. This is only of use for within converters. @@ -143,7 +141,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): invoked_subcommand: Optional[Command[Any, ..., Any]] = None, subcommand_passed: Optional[str] = None, command_failed: bool = False, - current_parameter: Optional[inspect.Parameter] = None, + current_parameter: Optional[Parameter] = None, current_argument: Optional[str] = None, ): self.message: Message = message @@ -158,7 +156,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): self.invoked_subcommand: Optional[Command[Any, ..., Any]] = invoked_subcommand self.subcommand_passed: Optional[str] = subcommand_passed self.command_failed: bool = command_failed - self.current_parameter: Optional[inspect.Parameter] = current_parameter + self.current_parameter: Optional[Parameter] = current_parameter self.current_argument: Optional[str] = current_argument self._state: ConnectionState = self.message._state @@ -357,7 +355,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): Any The result of the help command, if any. """ - from .core import Group, Command, wrap_callback + from .core import Command, Group, wrap_callback from .errors import CommandError bot = self.bot diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 28de883a9..bd24b6a76 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -24,35 +24,36 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -import re import inspect +import re from typing import ( + TYPE_CHECKING, Any, Dict, Generic, Iterable, + List, Literal, Optional, - TYPE_CHECKING, - List, Protocol, + Tuple, Type, TypeVar, - Tuple, Union, runtime_checkable, ) import discord + from .errors import * if TYPE_CHECKING: - from .context import Context from discord.state import Channel from discord.threads import Thread + from .parameters import Parameter from ._types import BotT, _Bot - + from .context import Context __all__ = ( 'Converter', @@ -1062,16 +1063,6 @@ def _convert_to_bool(argument: str) -> bool: raise BadBoolArgument(lowered) -def get_converter(param: inspect.Parameter) -> Any: - converter = param.annotation - if converter is param.empty: - if param.default is not param.empty: - converter = str if param.default is None else type(param.default) - else: - converter = str - return converter - - _GenericAlias = type(List[T]) @@ -1141,7 +1132,7 @@ async def _actual_conversion(ctx: Context[BotT], converter, argument: str, param raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc -async def run_converters(ctx: Context[BotT], converter: Any, argument: str, param: inspect.Parameter) -> Any: +async def run_converters(ctx: Context[BotT], converter: Any, argument: str, param: Parameter) -> Any: """|coro| Runs converters for a given converter, argument, and parameter. @@ -1158,7 +1149,7 @@ async def run_converters(ctx: Context[BotT], converter: Any, argument: str, para The converter to run, this corresponds to the annotation in the function. argument: :class:`str` The argument to convert to. - param: :class:`inspect.Parameter` + param: :class:`Parameter` The parameter being converted. This is mainly for error reporting. Raises @@ -1183,7 +1174,7 @@ async def run_converters(ctx: Context[BotT], converter: Any, argument: str, para # with the other parameters if conv is _NoneType and param.kind != param.VAR_POSITIONAL: ctx.view.undo() - return None if param.default is param.empty else param.default + return None if param.required else await param.get_default(ctx) try: value = await run_converters(ctx, conv, argument, param) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 026a84bac..8142c523d 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -23,54 +23,44 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations +import asyncio +import datetime +import functools +import inspect from typing import ( + TYPE_CHECKING, Any, Callable, Dict, Generator, Generic, - Literal, List, + Literal, Optional, - Union, Set, Tuple, - TypeVar, Type, - TYPE_CHECKING, + TypeVar, + Union, overload, ) -import asyncio -import functools -import inspect -import datetime import discord -from .errors import * -from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping -from .converter import run_converters, get_converter, Greedy from ._types import _BaseCommand from .cog import Cog from .context import Context - +from .converter import Greedy, run_converters +from .cooldowns import BucketType, Cooldown, CooldownMapping, DynamicCooldownMapping, MaxConcurrency +from .errors import * +from .parameters import Parameter, Signature if TYPE_CHECKING: - from typing_extensions import Concatenate, ParamSpec, TypeGuard, Self + from typing_extensions import Concatenate, ParamSpec, Self, TypeGuard from discord.message import Message - from ._types import ( - BotT, - ContextT, - Coro, - CoroFunc, - Check, - Hook, - Error, - ErrorT, - HookT, - ) + from ._types import BotT, Check, ContextT, Coro, CoroFunc, Error, ErrorT, Hook, HookT __all__ = ( @@ -131,9 +121,9 @@ def get_signature_parameters( /, *, skip_parameters: Optional[int] = None, -) -> Dict[str, inspect.Parameter]: - signature = inspect.signature(function) - params = {} +) -> Dict[str, Parameter]: + signature = Signature.from_callable(function) + params: Dict[str, Parameter] = {} cache: Dict[str, Any] = {} eval_annotation = discord.utils.evaluate_annotation required_params = discord.utils.is_inside_class(function) + 1 if skip_parameters is None else skip_parameters @@ -145,10 +135,25 @@ def get_signature_parameters( next(iterator) for name, parameter in iterator: + default = parameter.default + if isinstance(default, Parameter): # update from the default + if default.annotation is not Parameter.empty: + # There are a few cases to care about here. + # x: TextChannel = commands.CurrentChannel + # x = commands.CurrentChannel + # In both of these cases, the default parameter has an explicit annotation + # but in the second case it's only used as the fallback. + if default._fallback: + if parameter.annotation is Parameter.empty: + parameter._annotation = default.annotation + else: + parameter._annotation = default.annotation + + parameter._default = default.default + parameter._displayed_default = default._displayed_default + annotation = parameter.annotation - if annotation is parameter.empty: - params[name] = parameter - continue + if annotation is None: params[name] = parameter.replace(annotation=type(None)) continue @@ -435,7 +440,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): except AttributeError: globalns = {} - self.params: Dict[str, inspect.Parameter] = get_signature_parameters(function, globalns) + self.params: Dict[str, Parameter] = get_signature_parameters(function, globalns) def add_check(self, func: Check[ContextT], /) -> None: """Adds a check to the command. @@ -571,9 +576,8 @@ class Command(_BaseCommand, Generic[CogT, P, T]): finally: ctx.bot.dispatch('command_error', ctx, error) - async def transform(self, ctx: Context[BotT], param: inspect.Parameter, /) -> Any: - required = param.default is param.empty - converter = get_converter(param) + async def transform(self, ctx: Context[BotT], param: Parameter, /) -> Any: + converter = param.converter consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw view = ctx.view view.skip_ws() @@ -582,7 +586,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): # it undos the view ready for the next parameter to use instead if isinstance(converter, Greedy): if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): - return await self._transform_greedy_pos(ctx, param, required, converter.converter) + return await self._transform_greedy_pos(ctx, param, param.required, converter.converter) elif param.kind == param.VAR_POSITIONAL: return await self._transform_greedy_var_pos(ctx, param, converter.converter) else: @@ -594,13 +598,13 @@ class Command(_BaseCommand, Generic[CogT, P, T]): if view.eof: if param.kind == param.VAR_POSITIONAL: raise RuntimeError() # break the loop - if required: + if param.required: if self._is_typing_optional(param.annotation): return None if hasattr(converter, '__commands_is_flag__') and converter._can_be_constructible(): return await converter._construct_default(ctx) raise MissingRequiredArgument(param) - return param.default + return await param.get_default(ctx) previous = view.index if consume_rest_is_special: @@ -619,9 +623,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]): # type-checker fails to narrow argument return await run_converters(ctx, converter, argument, param) # type: ignore - async def _transform_greedy_pos( - self, ctx: Context[BotT], param: inspect.Parameter, required: bool, converter: Any - ) -> Any: + async def _transform_greedy_pos(self, ctx: Context[BotT], param: Parameter, required: bool, converter: Any) -> Any: view = ctx.view result = [] while not view.eof: @@ -639,10 +641,10 @@ class Command(_BaseCommand, Generic[CogT, P, T]): result.append(value) if not result and not required: - return param.default + return await param.get_default(ctx) return result - async def _transform_greedy_var_pos(self, ctx: Context[BotT], param: inspect.Parameter, converter: Any) -> Any: + async def _transform_greedy_var_pos(self, ctx: Context[BotT], param: Parameter, converter: Any) -> Any: view = ctx.view previous = view.index try: @@ -655,8 +657,8 @@ class Command(_BaseCommand, Generic[CogT, P, T]): return value @property - def clean_params(self) -> Dict[str, inspect.Parameter]: - """Dict[:class:`str`, :class:`inspect.Parameter`]: + def clean_params(self) -> Dict[str, Parameter]: + """Dict[:class:`str`, :class:`Parameter`]: Retrieves the parameter dictionary without the context or self parameters. Useful for inspecting signature. @@ -753,9 +755,8 @@ class Command(_BaseCommand, Generic[CogT, P, T]): elif param.kind == param.KEYWORD_ONLY: # kwarg only param denotes "consume rest" semantics if self.rest_is_raw: - converter = get_converter(param) ctx.current_argument = argument = view.read_rest() - kwargs[name] = await run_converters(ctx, converter, argument, param) + kwargs[name] = await run_converters(ctx, param.converter, argument, param) else: kwargs[name] = await self.transform(ctx, param) break @@ -1078,29 +1079,31 @@ class Command(_BaseCommand, Generic[CogT, P, T]): result = [] for name, param in params.items(): - greedy = isinstance(param.annotation, Greedy) + greedy = isinstance(param.converter, Greedy) optional = False # postpone evaluation of if it's an optional argument - # for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the - # parameter signature is a literal list of it's values - annotation = param.annotation.converter if greedy else param.annotation + annotation = param.converter.converter if greedy else param.converter # type: ignore # needs conditional types origin = getattr(annotation, '__origin__', None) if not greedy and origin is Union: none_cls = type(None) - union_args = annotation.__args__ + union_args = annotation.__args__ # type: ignore # this is safe optional = union_args[-1] is none_cls if len(union_args) == 2 and optional: annotation = union_args[0] origin = getattr(annotation, '__origin__', None) + # for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the + # parameter signature is a literal list of it's values if origin is Literal: - name = '|'.join(f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__) - if param.default is not param.empty: + name = '|'.join(f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__) # type: ignore # this is safe + if not param.required: # We don't want None or '' to trigger the [name=value] case and instead it should # do [name] since [name=None] or [name=] are not exactly useful for the user. should_print = param.default if isinstance(param.default, str) else param.default is not None if should_print: - result.append(f'[{name}={param.default}]' if not greedy else f'[{name}={param.default}]...') + result.append( + f'[{name}={param.displayed_default}]' if not greedy else f'[{name}={param.displayed_default}]...' + ) continue else: result.append(f'[{name}]') diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index d9e46d8bb..6ee925125 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -24,22 +24,21 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import Optional, Any, TYPE_CHECKING, List, Callable, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union from discord.errors import ClientException, DiscordException if TYPE_CHECKING: - from inspect import Parameter - - from .converter import Converter - from .context import Context - from .cooldowns import Cooldown, BucketType - from .flags import Flag from discord.abc import GuildChannel from discord.threads import Thread from discord.types.snowflake import Snowflake, SnowflakeList from ._types import BotT + from .context import Context + from .converter import Converter + from .cooldowns import BucketType, Cooldown + from .flags import Flag + from .parameters import Parameter __all__ = ( @@ -173,7 +172,7 @@ class MissingRequiredArgument(UserInputError): Attributes ----------- - param: :class:`inspect.Parameter` + param: :class:`Parameter` The argument that is missing. """ @@ -687,11 +686,11 @@ class MissingAnyRole(CheckFailure): missing = [f"'{role}'" for role in missing_roles] if len(missing) > 2: - fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1]) + fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1]) else: fmt = ' or '.join(missing) - message = f"You are missing at least one of the required roles: {fmt}" + message = f'You are missing at least one of the required roles: {fmt}' super().__init__(message) @@ -717,11 +716,11 @@ class BotMissingAnyRole(CheckFailure): missing = [f"'{role}'" for role in missing_roles] if len(missing) > 2: - fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1]) + fmt = '{}, or {}'.format(', '.join(missing[:-1]), missing[-1]) else: fmt = ' or '.join(missing) - message = f"Bot is missing at least one of the required roles: {fmt}" + message = f'Bot is missing at least one of the required roles: {fmt}' super().__init__(message) @@ -761,7 +760,7 @@ class MissingPermissions(CheckFailure): missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) + fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1]) else: fmt = ' and '.join(missing) message = f'You are missing {fmt} permission(s) to run this command.' @@ -786,7 +785,7 @@ class BotMissingPermissions(CheckFailure): missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_permissions] if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) + fmt = '{}, and {}'.format(', '.join(missing[:-1]), missing[-1]) else: fmt = ' and '.join(missing) message = f'Bot requires {fmt} permission(s) to run this command.' diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 64d57a145..b554df18e 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -24,37 +24,17 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from .errors import ( - BadFlagArgument, - CommandError, - MissingFlagArgument, - TooManyFlags, - MissingRequiredFlag, -) - -from discord.utils import resolve_annotation -from .view import StringView -from .converter import run_converters - -from discord.utils import maybe_coroutine, MISSING -from dataclasses import dataclass, field -from typing import ( - Dict, - Iterator, - Literal, - Optional, - Pattern, - Set, - TYPE_CHECKING, - Tuple, - List, - Any, - Union, -) - import inspect -import sys import re +import sys +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Literal, Optional, Pattern, Set, Tuple, Union + +from discord.utils import MISSING, maybe_coroutine, resolve_annotation + +from .converter import run_converters +from .errors import BadFlagArgument, CommandError, MissingFlagArgument, MissingRequiredFlag, TooManyFlags +from .view import StringView __all__ = ( 'Flag', @@ -66,9 +46,9 @@ __all__ = ( if TYPE_CHECKING: from typing_extensions import Self - from .context import Context - from ._types import BotT + from .context import Context + from .parameters import Parameter @dataclass @@ -123,6 +103,7 @@ def flag( default: Any = MISSING, max_args: int = MISSING, override: bool = MISSING, + converter: Any = MISSING, ) -> Any: """Override default functionality and parameters of the underlying :class:`FlagConverter` class attributes. @@ -144,8 +125,11 @@ def flag( override: :class:`bool` Whether multiple given values overrides the previous value. The default value depends on the annotation given. + converter: Any + The converter to use for this flag. This replaces the annotation at + runtime which is transparent to type checkers. """ - return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override) + return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override, annotation=converter) def validate_flag_name(name: str, forbidden: Set[str]) -> None: @@ -170,7 +154,8 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s for name, annotation in annotations.items(): flag = namespace.pop(name, MISSING) if isinstance(flag, Flag): - flag.annotation = annotation + if flag.annotation is MISSING: + flag.annotation = annotation else: flag = Flag(name=name, annotation=annotation, default=flag) @@ -351,7 +336,7 @@ class FlagsMeta(type): async def tuple_convert_all(ctx: Context[BotT], argument: str, flag: Flag, converter: Any) -> Tuple[Any, ...]: view = StringView(argument) results = [] - param: inspect.Parameter = ctx.current_parameter # type: ignore + param: Parameter = ctx.current_parameter # type: ignore while not view.eof: view.skip_ws() if view.eof: @@ -376,7 +361,7 @@ async def tuple_convert_all(ctx: Context[BotT], argument: str, flag: Flag, conve async def tuple_convert_flag(ctx: Context[BotT], argument: str, flag: Flag, converters: Any) -> Tuple[Any, ...]: view = StringView(argument) results = [] - param: inspect.Parameter = ctx.current_parameter # type: ignore + param: Parameter = ctx.current_parameter # type: ignore for converter in converters: view.skip_ws() if view.eof: @@ -402,7 +387,7 @@ async def tuple_convert_flag(ctx: Context[BotT], argument: str, flag: Flag, conv async def convert_flag(ctx: Context[BotT], argument: str, flag: Flag, annotation: Any = None) -> Any: - param: inspect.Parameter = ctx.current_parameter # type: ignore + param: Parameter = ctx.current_parameter # type: ignore annotation = annotation or flag.annotation try: origin = annotation.__origin__ diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 01458ab28..ca2dab52e 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -51,13 +51,13 @@ from .errors import CommandError if TYPE_CHECKING: from typing_extensions import Self - import inspect import discord.abc from .bot import BotBase from .context import Context from .cog import Cog + from .parameters import Parameter from ._types import ( Check, @@ -224,9 +224,7 @@ class _HelpCommandImpl(Command): super().__init__(inject.command_callback, *args, **kwargs) self._original: HelpCommand = inject self._injected: HelpCommand = inject - self.params: Dict[str, inspect.Parameter] = get_signature_parameters( - inject.command_callback, globals(), skip_parameters=1 - ) + self.params: Dict[str, Parameter] = get_signature_parameters(inject.command_callback, globals(), skip_parameters=1) async def prepare(self, ctx: Context[Any]) -> None: self._injected = injected = self._original.copy() @@ -1021,7 +1019,7 @@ class DefaultHelpCommand(HelpCommand): self.sort_commands: bool = options.pop('sort_commands', True) self.dm_help: bool = options.pop('dm_help', False) self.dm_help_threshold: int = options.pop('dm_help_threshold', 1000) - self.commands_heading: str = options.pop('commands_heading', "Commands:") + self.commands_heading: str = options.pop('commands_heading', 'Commands:') self.no_category: str = options.pop('no_category', 'No Category') self.paginator: Paginator = options.pop('paginator', None) @@ -1045,8 +1043,8 @@ class DefaultHelpCommand(HelpCommand): """:class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes.""" command_name = self.invoked_with return ( - f"Type {self.context.clean_prefix}{command_name} command for more info on a command.\n" - f"You can also type {self.context.clean_prefix}{command_name} category for more info on a category." + f'Type {self.context.clean_prefix}{command_name} command for more info on a command.\n' + f'You can also type {self.context.clean_prefix}{command_name} category for more info on a category.' ) def add_indented_commands( @@ -1235,10 +1233,10 @@ class MinimalHelpCommand(HelpCommand): def __init__(self, **options: Any) -> None: self.sort_commands: bool = options.pop('sort_commands', True) - self.commands_heading: str = options.pop('commands_heading', "Commands") + self.commands_heading: str = options.pop('commands_heading', 'Commands') self.dm_help: bool = options.pop('dm_help', False) self.dm_help_threshold: int = options.pop('dm_help_threshold', 1000) - self.aliases_heading: str = options.pop('aliases_heading', "Aliases:") + self.aliases_heading: str = options.pop('aliases_heading', 'Aliases:') self.no_category: str = options.pop('no_category', 'No Category') self.paginator: Paginator = options.pop('paginator', None) @@ -1268,8 +1266,8 @@ class MinimalHelpCommand(HelpCommand): """ command_name = self.invoked_with return ( - f"Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n" - f"You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category." + f'Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n' + f'You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category.' ) def get_command_signature(self, command: Command[Any, ..., Any], /) -> str: diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py new file mode 100644 index 000000000..e5a20aaa1 --- /dev/null +++ b/discord/ext/commands/parameters.py @@ -0,0 +1,270 @@ +""" +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 inspect +from operator import attrgetter +from typing import TYPE_CHECKING, Any, Literal, Optional, OrderedDict, Union, Protocol + +from discord.utils import MISSING, maybe_coroutine + +from .errors import NoPrivateMessage +from .converter import GuildConverter + +from discord import ( + Member, + User, + TextChannel, + VoiceChannel, + DMChannel, + Thread, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + from discord import Guild + + from .context import Context + +__all__ = ( + 'Parameter', + 'parameter', + 'param', + 'Author', + 'CurrentChannel', + 'CurrentGuild', +) + + +ParamKinds = Union[ + Literal[inspect.Parameter.POSITIONAL_ONLY], + Literal[inspect.Parameter.POSITIONAL_OR_KEYWORD], + Literal[inspect.Parameter.VAR_POSITIONAL], + Literal[inspect.Parameter.KEYWORD_ONLY], + Literal[inspect.Parameter.VAR_KEYWORD], +] + +empty: Any = inspect.Parameter.empty + + +def _gen_property(name: str) -> property: + attr = f'_{name}' + return property( + attrgetter(attr), + lambda self, value: setattr(self, attr, value), + doc=f"The parameter's {name}.", + ) + + +class Parameter(inspect.Parameter): + r"""A class that stores information on a :class:`Command`\'s parameter. + + This is a subclass of :class:`inspect.Parameter`. + + .. versionadded:: 2.0 + """ + + __slots__ = ('_displayed_default', '_fallback') + + def __init__( + self, + name: str, + kind: ParamKinds, + default: Any = empty, + annotation: Any = empty, + displayed_default: str = empty, + ) -> None: + super().__init__(name=name, kind=kind, default=default, annotation=annotation) + self._name = name + self._kind = kind + self._default = default + self._annotation = annotation + self._displayed_default = displayed_default + self._fallback = False + + def replace( + self, + *, + name: str = MISSING, # MISSING here cause empty is valid + kind: ParamKinds = MISSING, + default: Any = MISSING, + annotation: Any = MISSING, + displayed_default: Any = MISSING, + ) -> Self: + if name is MISSING: + name = self._name + if kind is MISSING: + kind = self._kind # type: ignore # this assignment is actually safe + if default is MISSING: + default = self._default + if annotation is MISSING: + annotation = self._annotation + if displayed_default is MISSING: + displayed_default = self._displayed_default + + return self.__class__( + name=name, + kind=kind, + default=default, + annotation=annotation, + displayed_default=displayed_default, + ) + + if not TYPE_CHECKING: # this is to prevent anything breaking if inspect internals change + name = _gen_property('name') + kind = _gen_property('kind') + default = _gen_property('default') + annotation = _gen_property('annotation') + + @property + def required(self) -> bool: + """:class:`bool`: Whether this parameter is required.""" + return self.default is empty + + @property + def converter(self) -> Any: + """The converter that should be used for this parameter.""" + if self.annotation is empty: + return type(self.default) if self.default not in (empty, None) else str + + return self.annotation + + @property + def displayed_default(self) -> Optional[str]: + """Optional[:class:`str`]: The displayed default in :class:`Command.signature`.""" + if self._displayed_default is not empty: + return self._displayed_default + + return None if self.required else str(self.default) + + async def get_default(self, ctx: Context[Any]) -> Any: + """|coro| + + Gets this parameter's default value. + + Parameters + ---------- + ctx: :class:`Context` + The invocation context that is used to get the default argument. + """ + # pre-condition: required is False + if callable(self.default): + return await maybe_coroutine(self.default, ctx) # type: ignore + return self.default + + +def parameter( + *, + converter: Any = empty, + default: Any = empty, + displayed_default: str = empty, +) -> Any: + r"""parameter(\*, converter=..., default=..., displayed_default=...) + + A way to assign custom metadata for a :class:`Command`\'s parameter. + + .. versionadded:: 2.0 + + Examples + -------- + A custom default can be used to have late binding behaviour. + + .. code-block:: python3 + + @bot.command() + async def wave(ctx, to: discord.User = commands.parameter(default=lambda ctx: ctx.author)): + await ctx.send(f'Hello {to.mention} :wave:') + + Parameters + ---------- + converter: Any + The converter to use for this parameter, this replaces the annotation at runtime which is transparent to type checkers. + default: Any + The default value for the parameter, if this is a :term:`callable` or a |coroutine_link|_ it is called with a + positional :class:`Context` argument. + displayed_default: :class:`str` + The displayed default in :attr:`Command.signature`. + """ + return Parameter( + name='empty', + kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=converter, + default=default, + displayed_default=displayed_default, + ) + + +class ParameterAlias(Protocol): + def __call__( + self, + *, + converter: Any = empty, + default: Any = empty, + displayed_default: str = empty, + ) -> Any: + ... + + +param: ParameterAlias = parameter +r"""param(\*, converter=..., default=..., displayed_default=...) + +An alias for :func:`parameter`. + +.. versionadded:: 2.0 +""" + +# some handy defaults +Author = parameter( + default=attrgetter('author'), + displayed_default='', + converter=Union[Member, User], +) +Author._fallback = True + +CurrentChannel = parameter( + default=attrgetter('channel'), + displayed_default='', + converter=Union[TextChannel, DMChannel, Thread, VoiceChannel], +) +CurrentChannel._fallback = True + + +def default_guild(ctx: Context[Any]) -> Guild: + if ctx.guild is not None: + return ctx.guild + raise NoPrivateMessage() + + +CurrentGuild = parameter( + default=default_guild, + displayed_default='', + converter=GuildConverter, +) + + +class Signature(inspect.Signature): + _parameter_cls = Parameter + parameters: OrderedDict[str, Parameter] diff --git a/discord/flags.py b/discord/flags.py index aae5b1e2e..140a63123 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -38,6 +38,7 @@ __all__ = ( 'PublicUserFlags', 'MemberCacheFlags', 'ApplicationFlags', + 'ChannelFlags', ) BF = TypeVar('BF', bound='BaseFlags') @@ -744,3 +745,39 @@ class ApplicationFlags(BaseFlags): def embedded_released(self): """:class:`bool`: Returns ``True`` if the embedded application is released to the public.""" return 1 << 1 + + +@fill_with_flags() +class ChannelFlags(BaseFlags): + r"""Wraps up the Discord :class:`~discord.abc.GuildChannel` or :class:`Thread` flags. + + .. container:: operations + + .. describe:: x == y + + Checks if two channel flags are equal. + .. describe:: x != y + + Checks if two channel flags are not equal. + .. 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. + + .. versionadded:: 2.0 + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @flag_value + def pinned(self): + """:class:`bool`: Returns ``True`` if the thread is pinned to the forum channel.""" + return 1 << 1 diff --git a/discord/guild.py b/discord/guild.py index 640b47b26..cf7abd727 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -114,7 +114,7 @@ if TYPE_CHECKING: ) from .types.voice import GuildVoiceState from .permissions import Permissions - from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel + from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel from .template import Template from .webhook import Webhook from .state import ConnectionState @@ -132,7 +132,7 @@ if TYPE_CHECKING: from .types.widget import EditWidgetSettings VocalGuildChannel = Union[VoiceChannel, StageChannel] - GuildChannel = Union[VocalGuildChannel, TextChannel, CategoryChannel] + GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] ByCategoryItem = Tuple[Optional[CategoryChannel], List[GuildChannel]] @@ -2061,7 +2061,7 @@ class Guild(Hashable): if before is not MISSING and after is not MISSING: raise TypeError('bans pagination does not support both before and after') - # This endpoint paginates in ascending order. + # This endpoint paginates in ascending order endpoint = self._state.http.get_bans async def _before_strategy(retrieve, before, limit): diff --git a/discord/http.py b/discord/http.py index 9960dc88f..b75f3f0fb 100644 --- a/discord/http.py +++ b/discord/http.py @@ -72,7 +72,7 @@ _log = logging.getLogger(__name__) if TYPE_CHECKING: from typing_extensions import Self - from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel + from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel from .handlers import CaptchaHandler from .threads import Thread from .file import File @@ -110,7 +110,7 @@ if TYPE_CHECKING: T = TypeVar('T') BE = TypeVar('BE', bound=BaseException) Response = Coroutine[Any, Any, T] - MessageableChannel = Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable, VoiceChannel] + MessageableChannel = Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel] async def json_or_text(response: aiohttp.ClientResponse) -> Union[Dict[str, Any], str]: @@ -162,6 +162,7 @@ def handle_message_parameters( stickers: Optional[SnowflakeList] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, mention_author: Optional[bool] = None, + extras: Dict[str, Any] = MISSING, ) -> MultipartParameters: if files is not MISSING and file is not MISSING: raise TypeError('Cannot mix file and files keyword arguments.') @@ -242,6 +243,9 @@ def handle_message_parameters( payload['attachments'] = attachments_payload + if extras is not MISSING: + payload.update(extras) + multipart = [] if files: multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) @@ -1017,6 +1021,7 @@ class HTTPClient: 'locked', 'invitable', 'default_auto_archive_duration', + 'flags', ) payload = {k: v for k, v in options.items() if k in valid_keys} return self.request(r, reason=reason, json=payload) @@ -1081,15 +1086,12 @@ class HTTPClient: ) payload = { 'name': name, + 'location': location if location is not MISSING else choice(('Message', 'Reply Chain Nudge')), 'auto_archive_duration': auto_archive_duration, - 'rate_limit_per_user': rate_limit_per_user, 'type': 11, } - - if location is MISSING: - payload['location'] = choice(('Message', 'Reply Chain Nudge')) - else: - payload['location'] = location + if rate_limit_per_user is not None: + payload['rate_limit_per_user'] = rate_limit_per_user return self.request(route, json=payload, reason=reason) @@ -1107,19 +1109,33 @@ class HTTPClient: r = Route('POST', '/channels/{channel_id}/threads', channel_id=channel_id) payload = { 'auto_archive_duration': auto_archive_duration, - 'location': None, + 'location': choice(('Plus Button', 'Thread Browser Toolbar')), 'name': name, 'type': type, - 'rate_limit_per_user': rate_limit_per_user, } if invitable is not MISSING: payload['invitable'] = invitable + if rate_limit_per_user is not None: + payload['rate_limit_per_user'] = rate_limit_per_user return self.request(r, json=payload, reason=reason) + def start_thread_in_forum( + self, + channel_id: Snowflake, + *, + params: MultipartParameters, + reason: Optional[str] = None, + ) -> Response[threads.Thread]: + r = Route('POST', '/channels/{channel_id}/threads', channel_id=channel_id) + if params.files: + return self.request(r, files=params.files, form=params.multipart, reason=reason) + else: + return self.request(r, json=params.payload, reason=reason) + def join_thread(self, channel_id: Snowflake) -> Response[None]: r = Route('POST', '/channels/{channel_id}/thread-members/@me', channel_id=channel_id) - params = {'location': choice(('Banner', 'Toolbar Overflow', 'Context Menu'))} + params = {'location': choice(('Banner', 'Toolbar Overflow', 'Sidebar Overflow', 'Context Menu'))} return self.request(r, params=params) @@ -1131,7 +1147,7 @@ class HTTPClient: def leave_thread(self, channel_id: Snowflake) -> Response[None]: r = Route('DELETE', '/channels/{channel_id}/thread-members/@me', channel_id=channel_id) - params = {'location': choice(('Toolbar Overflow', 'Context Menu'))} + params = {'location': choice(('Toolbar Overflow', 'Context Menu', 'Sidebar Overflow'))} return self.request(r, params=params) diff --git a/discord/member.py b/discord/member.py index 3c9e2c55f..c5eae8c1f 100644 --- a/discord/member.py +++ b/discord/member.py @@ -554,8 +554,6 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): @property def mention(self) -> str: """:class:`str`: Returns a string that allows you to mention the member.""" - if self.nick: - return f'<@!{self._user.id}>' return f'<@{self._user.id}>' @property @@ -968,7 +966,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): Raises ------- TypeError - The ``until`` parameter was the wrong type of the datetime was not timezone-aware. + The ``until`` parameter was the wrong type or the datetime was not timezone-aware. """ if until is None: diff --git a/discord/message.py b/discord/message.py index d27ace8c5..24a2047e8 100644 --- a/discord/message.py +++ b/discord/message.py @@ -538,6 +538,7 @@ class PartialMessage(Hashable): the constructor itself, and the second is via the following: - :meth:`TextChannel.get_partial_message` + - :meth:`VoiceChannel.get_partial_message` - :meth:`Thread.get_partial_message` - :meth:`DMChannel.get_partial_message` @@ -561,7 +562,7 @@ class PartialMessage(Hashable): Attributes ----------- - channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`Thread`, :class:`DMChannel`] + channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`] The channel associated with this partial message. id: :class:`int` The message ID. @@ -1159,7 +1160,7 @@ class Message(PartialMessage, Hashable): This is not stored long term within Discord's servers and is only used ephemerally. embeds: List[:class:`Embed`] A list of embeds the message has. - channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`] + channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`] 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. call: Optional[:class:`CallMessage`] @@ -1771,7 +1772,9 @@ class Message(PartialMessage, Hashable): return f'{self.author.name} just boosted the server **{self.content}** times! {self.guild} has achieved **Level 3!**' if self.type is MessageType.channel_follow_add: - return f'{self.author.name} has added **{self.content}** to this channel. Its most important updates will show up here.' + return ( + f'{self.author.name} has added {self.content} to this channel. Its most important updates will show up here.' + ) if self.type is MessageType.guild_stream: # The author will be a Member diff --git a/discord/permissions.py b/discord/permissions.py index 6cef95270..85e89a404 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -255,6 +255,27 @@ class Permissions(BaseFlags): """ return cls(0b1010000000000000000010000) + @classmethod + def elevated(cls) -> Self: + """A factory method that creates a :class:`Permissions` with all permissions + that require 2FA set to ``True``. These permissions are currently: + + - :attr:`kick_members` + - :attr:`ban_members` + - :attr:`administrator` + - :attr:`manage_channels` + - :attr:`manage_guild` + - :attr:`manage_messages` + - :attr:`manage_roles` + - :attr:`manage_webhooks` + - :attr:`manage_emojis_and_stickers` + - :attr:`manage_threads` + - :attr:`moderate_members` + + .. versionadded:: 2.0 + """ + return cls(0b10000010001110000000000000010000000111110) + @classmethod def advanced(cls) -> Self: """A factory method that creates a :class:`Permissions` with all diff --git a/discord/settings.py b/discord/settings.py index bc53a211a..831b81fe8 100644 --- a/discord/settings.py +++ b/discord/settings.py @@ -618,4 +618,3 @@ class GuildSettings: data = await self._state.http.edit_guild_settings(self._guild_id, payload) return GuildSettings(data=data, state=self._state) - diff --git a/discord/threads.py b/discord/threads.py index d2f0e7614..ba1c1151c 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -33,6 +33,7 @@ from .mixins import Hashable from .abc import Messageable, _purge_helper from .enums import ChannelType, try_enum from .errors import ClientException, InvalidData +from .flags import ChannelFlags from .utils import MISSING, parse_time, snowflake_time, _get_as_snowflake __all__ = ( @@ -51,7 +52,7 @@ if TYPE_CHECKING: ThreadArchiveDuration, ) from .guild import Guild - from .channel import TextChannel, CategoryChannel + from .channel import TextChannel, CategoryChannel, ForumChannel from .member import Member from .message import Message, PartialMessage from .abc import Snowflake, SnowflakeTime @@ -141,6 +142,7 @@ class Thread(Messageable, Hashable): 'archive_timestamp', '_member_ids', '_created_at', + '_flags', ) def __init__(self, *, guild: Guild, state: ConnectionState, data: ThreadPayload) -> None: @@ -172,6 +174,7 @@ class Thread(Messageable, Hashable): self.message_count: int = data['message_count'] self.member_count: int = data['member_count'] self._member_ids: List[Union[str, int]] = data['member_ids_preview'] + self._flags: int = data.get('flags', 0) self._unroll_metadata(data['thread_metadata']) def _unroll_metadata(self, data: ThreadMetadata): @@ -210,21 +213,26 @@ class Thread(Messageable, Hashable): return self._type @property - def parent(self) -> Optional[TextChannel]: - """Optional[:class:`TextChannel`]: The parent channel this thread belongs to. + def parent(self) -> Optional[Union[TextChannel, ForumChannel]]: + """Optional[Union[:class:`ForumChannel`, :class:`TextChannel`]]: The parent channel this thread belongs to. There is an alias for this named :attr:`channel`. """ return self.guild.get_channel(self.parent_id) # type: ignore @property - def channel(self) -> Optional[TextChannel]: - """Optional[:class:`TextChannel`]: The parent channel this thread belongs to. + def channel(self) -> Optional[Union[TextChannel, ForumChannel]]: + """Optional[Union[:class:`ForumChannel`, :class:`TextChannel`]]: The parent channel this thread belongs to. This is an alias of :attr:`parent`. """ return self.parent + @property + def flags(self) -> ChannelFlags: + """:class:`ChannelFlags`: The flags associated with this thread.""" + return ChannelFlags._from_value(self._flags) + @property def owner(self) -> Optional[Member]: """Optional[:class:`Member`]: The member this thread belongs to.""" @@ -499,6 +507,7 @@ class Thread(Messageable, Hashable): archived: bool = MISSING, locked: bool = MISSING, invitable: bool = MISSING, + pinned: bool = MISSING, slowmode_delay: int = MISSING, auto_archive_duration: ThreadArchiveDuration = MISSING, reason: Optional[str] = None, @@ -522,6 +531,8 @@ class Thread(Messageable, Hashable): Whether to archive the thread or not. locked: :class:`bool` Whether to lock the thread or not. + pinned: :class:`bool` + Whether to pin the thread or not. This only works if the thread is part of a forum. invitable: :class:`bool` Whether non-moderators can add other non-moderators to this thread. Only available for private threads. @@ -559,6 +570,10 @@ class Thread(Messageable, Hashable): payload['invitable'] = invitable if slowmode_delay is not MISSING: payload['rate_limit_per_user'] = slowmode_delay + if pinned is not MISSING: + flags = self.flags + flags.pinned = pinned + payload['flags'] = flags.value data = await self._state.http.edit_channel(self.id, **payload, reason=reason) # The data payload will always be a Thread payload diff --git a/discord/types/channel.py b/discord/types/channel.py index 3c02259f4..65f16f80a 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -40,7 +40,7 @@ class PermissionOverwrite(TypedDict): deny: str -ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13] +ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13, 15] ChannelType = Union[ChannelTypeWithoutThread, ThreadType] @@ -116,9 +116,14 @@ class ThreadChannel(_BaseChannel): rate_limit_per_user: NotRequired[int] last_message_id: NotRequired[Optional[Snowflake]] last_pin_timestamp: NotRequired[str] + flags: NotRequired[int] -GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel] +class ForumChannel(_BaseTextChannel): + type: Literal[15] + + +GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel] class DMChannel(_BaseChannel): diff --git a/discord/types/threads.py b/discord/types/threads.py index 44d3f1711..72fae1c3b 100644 --- a/discord/types/threads.py +++ b/discord/types/threads.py @@ -65,6 +65,8 @@ class Thread(TypedDict): member: NotRequired[ThreadMember] last_message_id: NotRequired[Optional[Snowflake]] last_pin_timestamp: NotRequired[Optional[Snowflake]] + newly_created: NotRequired[bool] + flags: NotRequired[int] class ThreadPaginationPayload(TypedDict): diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 57c03f56a..53d25981c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -43,6 +43,7 @@ from ..enums import try_enum, WebhookType from ..user import BaseUser, User from ..flags import MessageFlags from ..asset import Asset +from ..partial_emoji import PartialEmoji from ..http import Route, handle_message_parameters, HTTPClient from ..mixins import Hashable from ..channel import PartialMessageable @@ -67,6 +68,7 @@ if TYPE_CHECKING: from ..state import ConnectionState from ..http import Response from ..guild import Guild + from ..emoji import Emoji from ..channel import TextChannel from ..abc import Snowflake import datetime @@ -84,6 +86,7 @@ if TYPE_CHECKING: from ..types.channel import ( PartialChannel as PartialChannelPayload, ) + from ..types.emoji import PartialEmoji as PartialEmojiPayload BE = TypeVar('BE', bound=BaseException) _State = Union[ConnectionState, '_WebhookState'] @@ -467,6 +470,18 @@ class _WebhookState: # state parameter is artificial return BaseUser(state=self, data=data) # type: ignore + def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[PartialEmoji, Emoji, str]: + if self._parent is not None: + return self._parent.get_reaction_emoji(data) + + emoji_id = utils._get_as_snowflake(data, 'id') + + if not emoji_id: + # the name key will be a str + return data['name'] # type: ignore + + return PartialEmoji(animated=data.get('animated', False), id=emoji_id, name=data['name']) # type: ignore + @property def http(self) -> Union[HTTPClient, _FriendlyHttpAttributeErrorHelper]: if self._parent is not None: @@ -781,9 +796,10 @@ class Webhook(BaseWebhook): bot user or authentication. There are two main ways to use Webhooks. The first is through the ones - received by the library such as :meth:`.Guild.webhooks` and - :meth:`.TextChannel.webhooks`. The ones received by the library will - automatically be bound using the library's internal HTTP session. + received by the library such as :meth:`.Guild.webhooks`, + :meth:`.TextChannel.webhooks` and :meth:`.VoiceChannel.webhooks`. + The ones received by the library will automatically be + bound using the library's internal HTTP session. The second form involves creating a webhook object manually using the :meth:`~.Webhook.from_url` or :meth:`~.Webhook.partial` classmethods. diff --git a/docs/api.rst b/docs/api.rst index a45aa15a1..c0e6210bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -291,628 +291,125 @@ Connection or Discord terminating the connection one way or the other. This function can be called many times without a corresponding :func:`on_connect` call. -<<<<<<< HEAD - -.. function:: on_ready() - - 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. - - .. warning:: - - 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. - -.. function:: on_resumed() - - Called when the client has resumed a session. -======= - -.. function:: on_shard_connect(shard_id) - - Similar to :func:`on_connect` except used by :class:`AutoShardedClient` - to denote when a particular shard ID has connected to Discord. - - .. versionadded:: 1.4 - - :param shard_id: The shard ID that has connected. - :type shard_id: :class:`int` - - -.. function:: on_shard_disconnect(shard_id) - - Similar to :func:`on_disconnect` except used by :class:`AutoShardedClient` - to denote when a particular shard ID has disconnected from Discord. - - .. versionadded:: 1.4 - - :param shard_id: The shard ID that has disconnected. - :type shard_id: :class:`int` - -Debug -~~~~~~ ->>>>>>> upstream/master - -.. function:: on_error(event, *args, **kwargs) - - Usually when an event raises an uncaught exception, a traceback is - printed to stderr and the exception is ignored. If you want to - change this behaviour and handle the exception for whatever reason - yourself, this event can be overridden. Which, when done, will - suppress the default action of printing the traceback. - - The information of the exception raised and the exception itself can - be retrieved with a standard call to :func:`sys.exc_info`. - - If you want exception to propagate out of the :class:`Client` class - you can define an ``on_error`` handler consisting of a single empty - :ref:`raise statement `. Exceptions raised by ``on_error`` will not be - handled in any way by :class:`Client`. - - .. note:: - - ``on_error`` will only be dispatched to :meth:`Client.event`. - - It will not be received by :meth:`Client.wait_for`, or, if used, - :ref:`ext_commands_api_bot` listeners such as - :meth:`~ext.commands.Bot.listen` or :meth:`~ext.commands.Cog.listener`. - - :param event: The name of the event that raised the exception. - :type event: :class:`str` - - :param args: The positional arguments for the event that raised the - exception. - :param kwargs: The keyword arguments for the event that raised the - exception. - -.. function:: on_socket_event_type(event_type) - - Called whenever a websocket event is received from the WebSocket. - - This is mainly useful for logging how many events you are receiving - from the Discord gateway. - - .. versionadded:: 2.0 - - :param event_type: The event type from Discord that is received, e.g. ``'READY'``. - :type event_type: :class:`str` - -.. function:: on_socket_raw_receive(msg) - - Called whenever a message is completely received from the WebSocket, before - it's processed and parsed. This event is always dispatched when a - complete message is received and the passed data is not parsed in any way. - - This is only really useful for grabbing the WebSocket stream and - debugging purposes. - - This requires setting the ``enable_debug_events`` setting in the :class:`Client`. - - .. note:: - - This is only for the messages received from the client - WebSocket. The voice WebSocket will not trigger this event. - - :param msg: The message passed in from the WebSocket library. - :type msg: :class:`str` - -.. function:: on_socket_raw_send(payload) - - Called whenever a send operation is done on the WebSocket before the - message is sent. The passed parameter is the message that is being - sent to the WebSocket. - - This is only really useful for grabbing the WebSocket stream and - debugging purposes. - - This requires setting the ``enable_debug_events`` setting in the :class:`Client`. - - .. note:: - - This is only for the messages sent from the client - WebSocket. The voice WebSocket will not trigger this event. - - :param payload: The message that is about to be passed on to the - WebSocket library. It can be :class:`bytes` to denote a binary - message or :class:`str` to denote a regular text message. - - -Gateway -~~~~~~~~ - -<<<<<<< HEAD - The ``channel`` parameter can be a :class:`abc.Messageable` instance. - Which could either be :class:`TextChannel`, :class:`GroupChannel`, :class:`Thread`, or - :class:`DMChannel`. - - If the ``channel`` is a :class:`TextChannel` or :class:`Thread` then the ``user`` parameter - is a :class:`Member`, otherwise it is a :class:`User`. - - - - :param channel: The location where the typing originated from. - :type channel: :class:`abc.Messageable` - :param user: The user that started typing. - :type user: Union[:class:`User`, :class:`Member`] - :param when: When the typing started as an aware datetime in UTC. - :type when: :class:`datetime.datetime` - -.. function:: on_message(message) - - Called when a :class:`Message` is created and sent. - - -======= -.. function:: on_ready() - - 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. ->>>>>>> upstream/master - - .. warning:: - - 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. - -.. function:: on_resumed() - - Called when the client has resumed a session. - -.. function:: on_shard_ready(shard_id) - - Similar to :func:`on_ready` except used by :class:`AutoShardedClient` - to denote when a particular shard ID has become ready. - -<<<<<<< HEAD - -======= - :param shard_id: The shard ID that is ready. - :type shard_id: :class:`int` ->>>>>>> upstream/master - - -.. function:: on_shard_resumed(shard_id) - -<<<<<<< HEAD - 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. - - If this occurs increase the :class:`max_messages ` parameter - or use the :func:`on_raw_bulk_message_delete` event instead. - - - - :param messages: The messages that have been deleted. - :type messages: List[:class:`Message`] - -.. function:: on_raw_message_delete(payload) - - 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. - - If the message is found in the message cache, - it can be accessed via :attr:`RawMessageDeleteEvent.cached_message` - - - - :param payload: The raw event payload data. - :type payload: :class:`RawMessageDeleteEvent` - -.. function:: on_raw_bulk_message_delete(payload) - - 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. - - If the messages are found in the message cache, - they can be accessed via :attr:`RawBulkMessageDeleteEvent.cached_messages` - - - - :param payload: The raw event payload data. - :type payload: :class:`RawBulkMessageDeleteEvent` - -.. function:: on_message_edit(before, after) - - 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. - - If this occurs increase the :class:`max_messages ` parameter - or use the :func:`on_raw_message_edit` event instead. - - The following non-exhaustive cases trigger this event: - - - A message has been pinned or unpinned. - - The message content has been changed. - - The message has received an embed. - - - For performance reasons, the embed server does not do this in a "consistent" manner. - - - The message's embeds were suppressed or unsuppressed. - - A call message has received an update to its participants or ending time. - - - - :param before: The previous version of the message. - :type before: :class:`Message` - :param after: The current version of the message. - :type after: :class:`Message` - -.. function:: on_raw_message_edit(payload) - - Called when a message is edited. Unlike :func:`on_message_edit`, this is called - regardless of the state of the internal message cache. - - 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. - - Due to the inherently raw nature of this event, the data parameter coincides with - the raw data given by the `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 - denotes an "embed" only edit, which is an edit in which only the embeds are updated by the Discord - embed server. - - - - :param payload: The raw event payload data. - :type payload: :class:`RawMessageUpdateEvent` - -.. function:: on_reaction_add(reaction, user) - - 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. - - .. note:: - - To get the :class:`Message` being reacted, access it via :attr:`Reaction.message`. - - - - .. - - todo: check this out - - 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. - - :param reaction: The current state of the reaction. - :type reaction: :class:`Reaction` - :param user: The user who added the reaction. - :type user: Union[:class:`Member`, :class:`User`] - -.. function:: on_raw_reaction_add(payload) - - 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. - - - - :param payload: The raw event payload data. - :type payload: :class:`RawReactionActionEvent` - -.. function:: on_reaction_remove(reaction, user) - - 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. - - .. note:: - - To get the message being reacted, access it via :attr:`Reaction.message`. - - This requires both :attr:`Intents.reactions` and :attr:`Intents.members` to be enabled. - - .. note:: - - Consider using :func:`on_raw_reaction_remove` if you need this and do not want - to enable the members intent. - - :param reaction: The current state of the reaction. - :type reaction: :class:`Reaction` - :param user: The user who added the reaction. - :type user: Union[:class:`Member`, :class:`User`] - -.. function:: on_raw_reaction_remove(payload) - - 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. - - - - :param payload: The raw event payload data. - :type payload: :class:`RawReactionActionEvent` - -.. function:: on_reaction_clear(message, reactions) - - 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. - - - - :param message: The message that had its reactions cleared. - :type message: :class:`Message` - :param reactions: The reactions that were removed. - :type reactions: List[:class:`Reaction`] - -.. function:: on_raw_reaction_clear(payload) - - 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. - - - - :param payload: The raw event payload data. - :type payload: :class:`RawReactionClearEvent` - -.. function:: on_reaction_clear_emoji(reaction) - - 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. - - - - .. versionadded:: 1.3 - - :param reaction: The reaction that got cleared. - :type reaction: :class:`Reaction` - -.. function:: on_raw_reaction_clear_emoji(payload) - - 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. - - .. versionadded:: 1.3 - - :param payload: The raw event payload data. - :type payload: :class:`RawReactionClearEmojiEvent` - -.. function:: on_private_channel_update(before, after) - - Called whenever a private group DM is updated. e.g. changed name or topic. - - :param before: The updated group channel's old info. - :type before: :class:`GroupChannel` - :param after: The updated group channel's new info. - :type after: :class:`GroupChannel` - -.. function:: on_private_channel_pins_update(channel, last_pin) - - Called whenever a message is pinned or unpinned from a private channel. - - :param channel: The private channel that had its pins updated. - :type channel: :class:`abc.PrivateChannel` - :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``. - :type last_pin: Optional[:class:`datetime.datetime`] - -.. function:: on_guild_channel_delete(channel) - on_guild_channel_create(channel) - - Called whenever a guild channel is deleted or created. - - Note that you can get the guild from :attr:`~abc.GuildChannel.guild`. - - :param channel: The guild channel that got created or deleted. - :type channel: :class:`abc.GuildChannel` - -.. function:: on_guild_channel_update(before, after) - - Called whenever a guild channel is updated. e.g. changed name, topic, permissions. - - :param before: The updated guild channel's old info. - :type before: :class:`abc.GuildChannel` - :param after: The updated guild channel's new info. - :type after: :class:`abc.GuildChannel` - -.. function:: on_guild_channel_pins_update(channel, last_pin) - - Called whenever a message is pinned or unpinned from a guild channel. - - :param channel: The guild channel that had its pins updated. - :type channel: Union[:class:`abc.GuildChannel`, :class:`Thread`] - :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``. - :type last_pin: Optional[:class:`datetime.datetime`] - -.. function:: on_thread_join(thread) - - Called whenever a thread is joined or created. Note that from the API's perspective there is no way to - differentiate between a thread being created or the bot joining a thread. - - Note that you can get the guild from :attr:`Thread.guild`. - - .. versionadded:: 2.0 - - :param thread: The thread that got joined. - :type thread: :class:`Thread` - -.. function:: on_thread_remove(thread) - - Called whenever a thread is removed. This is different from a thread being deleted. - - Note that you can get the guild from :attr:`Thread.guild`. - - .. warning:: - - 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. - - .. versionadded:: 2.0 - - :param thread: The thread that got removed. - :type thread: :class:`Thread` - -.. function:: on_thread_delete(thread) - - Called whenever a thread is deleted. - - Note that you can get the guild from :attr:`Thread.guild`. - - .. versionadded:: 2.0 - - :param thread: The thread that got deleted. - :type thread: :class:`Thread` - -.. function:: on_thread_member_join(member) - on_thread_member_remove(member) - - Called when a :class:`ThreadMember` leaves or joins a :class:`Thread`. - - You can get the thread a member belongs in by accessing :attr:`ThreadMember.thread`. - - .. versionadded:: 2.0 - - :param member: The member who joined or left. - :type member: :class:`ThreadMember` - -.. function:: on_thread_update(before, after) - - Called whenever a thread is updated. - - .. versionadded:: 2.0 - - :param before: The updated thread's old info. - :type before: :class:`Thread` - :param after: The updated thread's new info. - :type after: :class:`Thread` - -.. function:: on_guild_integrations_update(guild) - Called whenever an integration is created, modified, or removed from a guild. -======= - Similar to :func:`on_resumed` except used by :class:`AutoShardedClient` - to denote when a particular shard ID has resumed a session. ->>>>>>> upstream/master +Debug +~~~~~~ - .. versionadded:: 1.4 +.. function:: on_error(event, *args, **kwargs) - :param shard_id: The shard ID that has resumed. - :type shard_id: :class:`int` + Usually when an event raises an uncaught exception, a traceback is + printed to stderr and the exception is ignored. If you want to + change this behaviour and handle the exception for whatever reason + yourself, this event can be overridden. Which, when done, will + suppress the default action of printing the traceback. -Guilds -~~~~~~~ + The information of the exception raised and the exception itself can + be retrieved with a standard call to :func:`sys.exc_info`. -.. function:: on_guild_available(guild) - on_guild_unavailable(guild) + If you want exception to propagate out of the :class:`Client` class + you can define an ``on_error`` handler consisting of a single empty + :ref:`raise statement `. Exceptions raised by ``on_error`` will not be + handled in any way by :class:`Client`. -<<<<<<< HEAD - .. versionadded:: 2.0 + .. note:: - :param integration: The integration that was created. - :type integration: :class:`Integration` + ``on_error`` will only be dispatched to :meth:`Client.event`. -.. function:: on_integration_update(integration) + It will not be received by :meth:`Client.wait_for`, or, if used, + :ref:`ext_commands_api_bot` listeners such as + :meth:`~ext.commands.Bot.listen` or :meth:`~ext.commands.Cog.listener`. - Called when an integration is updated. + :param event: The name of the event that raised the exception. + :type event: :class:`str` - .. versionadded:: 2.0 + :param args: The positional arguments for the event that raised the + exception. + :param kwargs: The keyword arguments for the event that raised the + exception. - :param integration: The integration that was created. - :type integration: :class:`Integration` +.. function:: on_socket_event_type(event_type) -.. function:: on_raw_integration_delete(payload) + Called whenever a websocket event is received from the WebSocket. - Called when an integration is deleted. + This is mainly useful for logging how many events you are receiving + from the Discord gateway. .. versionadded:: 2.0 - :param payload: The raw event payload data. - :type payload: :class:`RawIntegrationDeleteEvent` - -.. function:: on_webhooks_update(channel) - - Called whenever a webhook is created, modified, or removed from a guild channel. - - :param channel: The channel that had its webhooks updated. - :type channel: :class:`abc.GuildChannel` - -.. function:: on_member_join(member) - on_member_remove(member) + :param event_type: The event type from Discord that is received, e.g. ``'READY'``. + :type event_type: :class:`str` + +.. function:: on_socket_raw_receive(msg) - Called when a :class:`Member` leaves or joins a :class:`Guild`. + Called whenever a message is completely received from the WebSocket, before + it's processed and parsed. This event is always dispatched when a + complete message is received and the passed data is not parsed in any way. - :param member: The member who joined or left. - :type member: :class:`Member` + This is only really useful for grabbing the WebSocket stream and + debugging purposes. -.. function:: on_member_update(before, after) + This requires setting the ``enable_debug_events`` setting in the :class:`Client`. - Called when a :class:`Member` updates their profile. + .. note:: - This is called when one or more of the following things change: + This is only for the messages received from the client + WebSocket. The voice WebSocket will not trigger this event. - - nickname - - roles - - pending + :param msg: The message passed in from the WebSocket library. + :type msg: :class:`str` +.. function:: on_socket_raw_send(payload) + Called whenever a send operation is done on the WebSocket before the + message is sent. The passed parameter is the message that is being + sent to the WebSocket. - :param before: The updated member's old info. - :type before: :class:`Member` - :param after: The updated member's updated info. - :type after: :class:`Member` + This is only really useful for grabbing the WebSocket stream and + debugging purposes. -.. function:: on_presence_update(before, after) + This requires setting the ``enable_debug_events`` setting in the :class:`Client`. - Called when a :class:`Member` updates their presence. + .. note:: - This is called when one or more of the following things change: + This is only for the messages sent from the client + WebSocket. The voice WebSocket will not trigger this event. - - status - - activity + :param payload: The message that is about to be passed on to the + WebSocket library. It can be :class:`bytes` to denote a binary + message or :class:`str` to denote a regular text message. +Gateway +~~~~~~~~ - .. versionadded:: 2.0 +.. function:: on_ready() - :param before: The updated member's old info. - :type before: :class:`Member` - :param after: The updated member's updated info. - :type after: :class:`Member` + 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. -.. function:: on_user_update(before, after) + .. warning:: - Called when a :class:`User` updates their profile. + 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. - This is called when one or more of the following things change: +.. function:: on_resumed() - - avatar - - username - - discriminator + Called when the client has resumed a session. +Guilds +~~~~~~~ +.. function:: on_guild_available(guild) + on_guild_unavailable(guild) - :param before: The updated user's old info. - :type before: :class:`User` - :param after: The updated user's updated info. - :type after: :class:`User` -======= Called when a guild becomes available or unavailable. The guild must have existed in the :attr:`Client.guilds` cache. This requires :attr:`Intents.guilds` to be enabled. :param guild: The :class:`Guild` that has changed availability. ->>>>>>> upstream/master .. function:: on_guild_join(guild) @@ -959,32 +456,6 @@ Guilds :param after: The guild after being updated. :type after: :class:`Guild` -<<<<<<< HEAD -.. function:: on_guild_role_create(role) - on_guild_role_delete(role) - - Called when a :class:`Guild` creates or deletes a new :class:`Role`. - - To get the guild it belongs to, use :attr:`Role.guild`. - - - - :param role: The role that was created or deleted. - :type role: :class:`Role` - -.. function:: on_guild_role_update(before, after) - - Called when a :class:`Role` is changed guild-wide. - - - - :param before: The updated role's old info. - :type before: :class:`Role` - :param after: The updated role's updated info. - :type after: :class:`Role` - -======= ->>>>>>> upstream/master .. function:: on_guild_emojis_update(guild, before, after) Called when a :class:`Guild` adds or removes :class:`Emoji`. @@ -1493,10 +964,6 @@ Roles :param after: The updated role's updated info. :type after: :class:`Role` -<<<<<<< HEAD - -======= ->>>>>>> upstream/master Scheduled Events ~~~~~~~~~~~~~~~~~ @@ -1552,12 +1019,8 @@ Scheduled Events Stages ~~~~~~~ -<<<<<<< HEAD - -======= .. function:: on_stage_instance_create(stage_instance) on_stage_instance_delete(stage_instance) ->>>>>>> upstream/master Called when a :class:`StageInstance` is created or deleted for a :class:`StageChannel`. @@ -1566,11 +1029,7 @@ Stages :param stage_instance: The stage instance that was created or deleted. :type stage_instance: :class:`StageInstance` -<<<<<<< HEAD - -======= .. function:: on_stage_instance_update(before, after) ->>>>>>> upstream/master Called when a :class:`StageInstance` is updated. @@ -1589,15 +1048,29 @@ Stages Threads ~~~~~~~~ +<<<<<<< HEAD <<<<<<< HEAD ======= >>>>>>> upstream/master +======= +.. function:: on_thread_create(thread) + + Called whenever a thread is created. + + Note that you can get the guild from :attr:`Thread.guild`. + + This requires :attr:`Intents.guilds` to be enabled. + + .. versionadded:: 2.0 + + :param thread: The thread that was created. + :type thread: :class:`Thread` +>>>>>>> 95deb553328d4d1c3e8b6b50239b67c56c4576fa .. function:: on_thread_join(thread) - Called whenever a thread is joined or created. Note that from the API's perspective there is no way to - differentiate between a thread being created or the bot joining a thread. + Called whenever a thread is joined. Note that you can get the guild from :attr:`Thread.guild`. @@ -1800,6 +1273,12 @@ of :class:`enum.Enum`. .. versionadded:: 2.0 + .. attribute:: forum + + A forum channel. + + .. versionadded:: 2.0 + .. class:: MessageType Specifies the type of :class:`Message`. This is used to denote if a message @@ -4294,6 +3773,7 @@ TextChannel .. automethod:: typing :async-with: +<<<<<<< HEAD ChannelSettings ~~~~~~~~~~~~~~~~ @@ -4305,6 +3785,17 @@ ChannelSettings :inherited-members: +======= +ForumChannel +~~~~~~~~~~~~~ + +.. attributetable:: ForumChannel + +.. autoclass:: ForumChannel() + :members: + :inherited-members: + +>>>>>>> 95deb553328d4d1c3e8b6b50239b67c56c4576fa Thread ~~~~~~~~ @@ -4638,6 +4129,15 @@ ApplicationFlags .. autoclass:: ApplicationFlags :members: +ChannelFlags +~~~~~~~~~~~~~~ + +.. attributetable:: ChannelFlags + +.. autoclass:: ChannelFlags + :members: + + File ~~~~~ diff --git a/docs/crowdin.yml b/docs/crowdin.yml deleted file mode 100644 index d23bfe6d0..000000000 --- a/docs/crowdin.yml +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -project_id: "362783" -api_token_env: CROWDIN_API_KEY - -files: - - source: /_build/locale/**/*.pot - translation: /locale/%two_letters_code%/LC_MESSAGES/%original_path%/%file_name%.po - -# You must use `crowdin download --all` for this project -# I discovered after like an hour of debugging the Java CLI that `--all` actually means "use server sources" -# Without this, crowdin tries to determine the mapping itself, and decides that because -# `/locale/ja/LC_MESSAGES/_build/locale/...` doesn't exist, that it won't download anything -# There is no workaround for this. I tried. Trying to adjust the project base path just breaks things further. - -# Crowdin does the conflict resolution on its end. The process to update translations is thus: -# - make gettext -# - crowdin upload -# - crowdin download --all -# You must set ${CROWDIN_API_KEY} in the environment. -# I will write an Actions workflow for this at a later date. diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index da98cd734..be5e5864a 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -421,6 +421,35 @@ Flag Converter .. autofunction:: discord.ext.commands.flag + +Defaults +-------- + +.. autoclass:: discord.ext.commands.Parameter() + :members: + +.. autofunction:: discord.ext.commands.parameter + +.. autofunction:: discord.ext.commands.param + +.. data:: discord.ext.commands.Author + + A default :class:`.Parameter` which returns the :attr:`~.Context.author` for this context. + + .. versionadded:: 2.0 + +.. data:: discord.ext.commands.CurrentChannel + + A default :class:`.Parameter` which returns the :attr:`~.Context.channel` for this context. + + .. versionadded:: 2.0 + +.. data:: discord.ext.commands.CurrentGuild + + A default :class:`.Parameter` which returns the :attr:`~.Context.guild` for this context. This will never be ``None``. If the command is called in a DM context then :exc:`~discord.ext.commands.NoPrivateMessage` is raised to the error handlers. + + .. versionadded:: 2.0 + .. _ext_commands_api_errors: Exceptions diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 4fa97ae5c..d31809f58 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -768,6 +768,58 @@ A :class:`dict` annotation is functionally equivalent to ``List[Tuple[K, V]]`` e given as a :class:`dict` rather than a :class:`list`. +.. _ext_commands_parameter: + +Parameter Metadata +------------------- + +:func:`~ext.commands.parameter` assigns custom metadata to a :class:`~ext.commands.Command`'s parameter. + +This is useful for: + +- Custom converters as annotating a parameter with a custom converter works at runtime, type checkers don't like it + because they can't understand what's going on. + + .. code-block:: python3 + + class SomeType: + foo: int + + class MyVeryCoolConverter(commands.Converter[SomeType]): + ... # implementation left as an exercise for the reader + + @bot.command() + async def bar(ctx, cool_value: MyVeryCoolConverter): + cool_value.foo # type checker warns MyVeryCoolConverter has no value foo (uh-oh) + + However, fear not we can use :func:`~ext.commands.parameter` to tell type checkers what's going on. + + .. code-block:: python3 + + @bot.command() + async def bar(ctx, cool_value: SomeType = commands.parameter(converter=MyVeryCoolConverter)): + cool_value.foo # no error (hurray) + +- Late binding behaviour + + .. code-block:: python3 + + @bot.command() + async def wave(to: discord.User = commands.parameter(default=lambda ctx: ctx.author)): + await ctx.send(f'Hello {to.mention} :wave:') + + Because this is such a common use-case, the library provides :obj:`~.ext.commands.Author`, :obj:`~.ext.commands.CurrentChannel` and + :obj:`~.ext.commands.CurrentGuild`, armed with this we can simplify ``wave`` to: + + .. code-block:: python3 + + @bot.command() + async def wave(to: discord.User = commands.Author): + await ctx.send(f'Hello {to.mention} :wave:') + + :obj:`~.ext.commands.Author` and co also have other benefits like having the displayed default being filled. + + .. _ext_commands_error_handler: Error Handling diff --git a/docs/migrating.rst b/docs/migrating.rst index e6f3abbb4..db2817bfc 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -805,9 +805,32 @@ Quick example: With this change, constructor of :class:`Client` no longer accepts ``connector`` and ``loop`` parameters. -In parallel with this change, changes were made to loading and unloading of commands extension extensions and cogs, +In parallel with this change, changes were made to loading and unloading of commands extension extensions and cogs, see :ref:`migrating_2_0_commands_extension_cog_async` for more information. +Intents Are Now Required +-------------------------- + +In earlier versions, the ``intents`` keyword argument was optional and defaulted to :meth:`Intents.default`. In order to better educate users on their intents and to also make it more explicit, this parameter is now required to pass in. + +For example: + +.. code-block:: python3 + + # before + client = discord.Client() + + # after + intents = discord.Intents.default() + client = discord.Client(intents=intents) + +This change applies to **all** subclasses of :class:`Client`. + +- :class:`AutoShardedClient` +- :class:`~discord.ext.commands.Bot` +- :class:`~discord.ext.commands.AutoShardedBot` + + Abstract Base Classes Changes ------------------------------- @@ -1933,7 +1956,7 @@ Quick example of loading an extension: async with bot: await bot.load_extension('my_extension') await bot.start(TOKEN) - + asyncio.run(main()) @@ -2115,6 +2138,7 @@ Miscellaneous Changes - ``BotMissingPermissions.missing_perms`` has been renamed to :attr:`ext.commands.BotMissingPermissions.missing_permissions`. - :meth:`ext.commands.Cog.cog_load` has been added as part of the :ref:`migrating_2_0_commands_extension_cog_async` changes. - :meth:`ext.commands.Cog.cog_unload` may now be a :term:`coroutine` due to the :ref:`migrating_2_0_commands_extension_cog_async` changes. +- :attr:`ext.commands.Command.clean_params` type now uses a custom :class:`inspect.Parameter` to handle defaults. .. _migrating_2_0_tasks: