Browse Source

Rebase to latest upstream

pull/10109/head
dolfies 3 years ago
parent
commit
8fcca3183a
  1. 22
      .github/workflows/build.yml
  2. 28
      .github/workflows/scripts/close_and_reopen_pr.js
  3. 3
      discord/abc.py
  4. 347
      discord/channel.py
  5. 24
      discord/embeds.py
  6. 1
      discord/enums.py
  7. 9
      discord/ext/commands/__init__.py
  8. 3
      discord/ext/commands/bot.py
  9. 18
      discord/ext/commands/context.py
  10. 29
      discord/ext/commands/converter.py
  11. 111
      discord/ext/commands/core.py
  12. 27
      discord/ext/commands/errors.py
  13. 57
      discord/ext/commands/flags.py
  14. 20
      discord/ext/commands/help.py
  15. 270
      discord/ext/commands/parameters.py
  16. 37
      discord/flags.py
  17. 6
      discord/guild.py
  18. 40
      discord/http.py
  19. 4
      discord/member.py
  20. 9
      discord/message.py
  21. 21
      discord/permissions.py
  22. 1
      discord/settings.py
  23. 25
      discord/threads.py
  24. 9
      discord/types/channel.py
  25. 2
      discord/types/threads.py
  26. 22
      discord/webhook/async_.py
  27. 732
      docs/api.rst
  28. 21
      docs/crowdin.yml
  29. 29
      docs/ext/commands/api.rst
  30. 52
      docs/ext/commands/commands.rst
  31. 28
      docs/migrating.rst

22
.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/*

28
.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'
});
})

3
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

347
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

24
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.

1
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

9
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 *

3
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``.

18
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

29
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)

111
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}]')

27
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.'

57
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__

20
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:

270
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='<you>',
converter=Union[Member, User],
)
Author._fallback = True
CurrentChannel = parameter(
default=attrgetter('channel'),
displayed_default='<this channel>',
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='<this server>',
converter=GuildConverter,
)
class Signature(inspect.Signature):
_parameter_cls = Parameter
parameters: OrderedDict[str, Parameter]

37
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

6
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):

40
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)

4
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:

9
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

21
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

1
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)

25
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

9
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):

2
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):

22
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.

732
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 <py:raise>`. 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 <Client>` 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 <Client>` 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 <https://discord.com/developers/docs/topics/gateway#message-update>`_.
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 <py:raise>`. 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
~~~~~

21
docs/crowdin.yml

@ -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.

29
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

52
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

28
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:

Loading…
Cancel
Save