Browse Source

Merge branch 'master' into feature/guild_onboarding

pull/9260/head
Josh 2 years ago
parent
commit
5307f1a6eb
  1. 4
      discord/__init__.py
  2. 15
      discord/abc.py
  3. 42
      discord/appinfo.py
  4. 15
      discord/audit_logs.py
  5. 56
      discord/automod.py
  6. 261
      discord/channel.py
  7. 14
      discord/client.py
  8. 5
      discord/colour.py
  9. 16
      discord/enums.py
  10. 13
      discord/ext/commands/cog.py
  11. 170
      discord/ext/commands/context.py
  12. 2
      discord/ext/commands/converter.py
  13. 7
      discord/ext/commands/errors.py
  14. 11
      discord/ext/commands/hybrid.py
  15. 15
      discord/gateway.py
  16. 300
      discord/guild.py
  17. 13
      discord/http.py
  18. 5
      discord/interactions.py
  19. 105
      discord/message.py
  20. 46
      discord/permissions.py
  21. 120
      discord/scheduled_event.py
  22. 2
      discord/sticker.py
  23. 17
      discord/types/appinfo.py
  24. 6
      discord/types/automod.py
  25. 2
      discord/types/channel.py
  26. 4
      discord/types/guild.py
  27. 2
      discord/types/sticker.py
  28. 2
      discord/ui/item.py
  29. 14
      discord/ui/select.py
  30. 12
      discord/webhook/sync.py
  31. 2
      discord/widget.py
  32. 86
      docs/api.rst
  33. 2
      docs/ext/commands/api.rst
  34. 1
      docs/ext/commands/commands.rst
  35. 90
      docs/whats_new.rst

4
discord/__init__.py

@ -13,7 +13,7 @@ __title__ = 'discord'
__author__ = 'Rapptz' __author__ = 'Rapptz'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz' __copyright__ = 'Copyright 2015-present Rapptz'
__version__ = '2.2.0a' __version__ = '2.3.0a'
__path__ = __import__('pkgutil').extend_path(__path__, __name__) __path__ = __import__('pkgutil').extend_path(__path__, __name__)
@ -79,7 +79,7 @@ class VersionInfo(NamedTuple):
serial: int serial: int
version_info: VersionInfo = VersionInfo(major=2, minor=2, micro=0, releaselevel='alpha', serial=0) version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=0, releaselevel='alpha', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())

15
discord/abc.py

@ -83,7 +83,15 @@ if TYPE_CHECKING:
from .channel import CategoryChannel from .channel import CategoryChannel
from .embeds import Embed from .embeds import Embed
from .message import Message, MessageReference, PartialMessage from .message import Message, MessageReference, PartialMessage
from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel from .channel import (
TextChannel,
DMChannel,
GroupChannel,
PartialMessageable,
VocalGuildChannel,
VoiceChannel,
StageChannel,
)
from .threads import Thread from .threads import Thread
from .enums import InviteTarget from .enums import InviteTarget
from .ui.view import View from .ui.view import View
@ -97,7 +105,7 @@ if TYPE_CHECKING:
SnowflakeList, SnowflakeList,
) )
PartialMessageableChannel = Union[TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable] PartialMessageableChannel = Union[TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable]
MessageableChannel = Union[PartialMessageableChannel, GroupChannel] MessageableChannel = Union[PartialMessageableChannel, GroupChannel]
SnowflakeTime = Union["Snowflake", datetime] SnowflakeTime = Union["Snowflake", datetime]
@ -118,7 +126,7 @@ async def _single_delete_strategy(messages: Iterable[Message], *, reason: Option
async def _purge_helper( async def _purge_helper(
channel: Union[Thread, TextChannel, VoiceChannel], channel: Union[Thread, TextChannel, VocalGuildChannel],
*, *,
limit: Optional[int] = 100, limit: Optional[int] = 100,
check: Callable[[Message], bool] = MISSING, check: Callable[[Message], bool] = MISSING,
@ -1284,6 +1292,7 @@ class Messageable:
- :class:`~discord.TextChannel` - :class:`~discord.TextChannel`
- :class:`~discord.VoiceChannel` - :class:`~discord.VoiceChannel`
- :class:`~discord.StageChannel`
- :class:`~discord.DMChannel` - :class:`~discord.DMChannel`
- :class:`~discord.GroupChannel` - :class:`~discord.GroupChannel`
- :class:`~discord.PartialMessageable` - :class:`~discord.PartialMessageable`

42
discord/appinfo.py

@ -166,7 +166,7 @@ class AppInfo:
self.name: str = data['name'] self.name: str = data['name']
self.description: str = data['description'] self.description: str = data['description']
self._icon: Optional[str] = data['icon'] self._icon: Optional[str] = data['icon']
self.rpc_origins: List[str] = data['rpc_origins'] self.rpc_origins: Optional[List[str]] = data.get('rpc_origins')
self.bot_public: bool = data['bot_public'] self.bot_public: bool = data['bot_public']
self.bot_require_code_grant: bool = data['bot_require_code_grant'] self.bot_require_code_grant: bool = data['bot_require_code_grant']
self.owner: User = state.create_user(data['owner']) self.owner: User = state.create_user(data['owner'])
@ -255,6 +255,24 @@ class PartialAppInfo:
The application's terms of service URL, if set. The application's terms of service URL, if set.
privacy_policy_url: Optional[:class:`str`] privacy_policy_url: Optional[:class:`str`]
The application's privacy policy URL, if set. The application's privacy policy URL, if set.
approximate_guild_count: :class:`int`
The approximate count of the guilds the bot was added to.
.. versionadded:: 2.3
redirect_uris: List[:class:`str`]
A list of authentication redirect URIs.
.. versionadded:: 2.3
interactions_endpoint_url: Optional[:class:`str`]
The interactions endpoint url of the application to receive interactions over this endpoint rather than
over the gateway, if configured.
.. versionadded:: 2.3
role_connections_verification_url: Optional[:class:`str`]
The application's connection verification URL which will render the application as
a verification method in the guild's role verification configuration.
.. versionadded:: 2.3
""" """
__slots__ = ( __slots__ = (
@ -268,6 +286,11 @@ class PartialAppInfo:
'privacy_policy_url', 'privacy_policy_url',
'_icon', '_icon',
'_flags', '_flags',
'_cover_image',
'approximate_guild_count',
'redirect_uris',
'interactions_endpoint_url',
'role_connections_verification_url',
) )
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload): def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
@ -276,11 +299,16 @@ class PartialAppInfo:
self.name: str = data['name'] self.name: str = data['name']
self._icon: Optional[str] = data.get('icon') self._icon: Optional[str] = data.get('icon')
self._flags: int = data.get('flags', 0) self._flags: int = data.get('flags', 0)
self._cover_image: Optional[str] = data.get('cover_image')
self.description: str = data['description'] self.description: str = data['description']
self.rpc_origins: Optional[List[str]] = data.get('rpc_origins') self.rpc_origins: Optional[List[str]] = data.get('rpc_origins')
self.verify_key: str = data['verify_key'] self.verify_key: str = data['verify_key']
self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url') self.terms_of_service_url: Optional[str] = data.get('terms_of_service_url')
self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url') self.privacy_policy_url: Optional[str] = data.get('privacy_policy_url')
self.approximate_guild_count: int = data.get('approximate_guild_count', 0)
self.redirect_uris: List[str] = data.get('redirect_uris', [])
self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url')
self.role_connections_verification_url: Optional[str] = data.get('role_connections_verification_url')
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>' return f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>'
@ -292,6 +320,18 @@ class PartialAppInfo:
return None return None
return Asset._from_icon(self._state, self.id, self._icon, path='app') return Asset._from_icon(self._state, self.id, self._icon, path='app')
@property
def cover_image(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the cover image of the application's default rich presence.
This is only available if the application is a game sold on Discord.
.. versionadded:: 2.3
"""
if self._cover_image is None:
return None
return Asset._from_cover_image(self._state, self.id, self._cover_image)
@property @property
def flags(self) -> ApplicationFlags: def flags(self) -> ApplicationFlags:
""":class:`ApplicationFlags`: The application's flags. """:class:`ApplicationFlags`: The application's flags.

15
discord/audit_logs.py

@ -521,7 +521,7 @@ class _AuditLogProxyMessageBulkDelete(_AuditLogProxy):
class _AuditLogProxyAutoModAction(_AuditLogProxy): class _AuditLogProxyAutoModAction(_AuditLogProxy):
automod_rule_name: str automod_rule_name: str
automod_rule_trigger_type: str automod_rule_trigger_type: str
channel: Union[abc.GuildChannel, Thread] channel: Optional[Union[abc.GuildChannel, Thread]]
class AuditLogEntry(Hashable): class AuditLogEntry(Hashable):
@ -644,13 +644,17 @@ class AuditLogEntry(Hashable):
or self.action is enums.AuditLogAction.automod_flag_message or self.action is enums.AuditLogAction.automod_flag_message
or self.action is enums.AuditLogAction.automod_timeout_member or self.action is enums.AuditLogAction.automod_timeout_member
): ):
channel_id = int(extra['channel_id']) channel_id = utils._get_as_snowflake(extra, 'channel_id')
channel = None
if channel_id is not None:
channel = self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id)
self.extra = _AuditLogProxyAutoModAction( self.extra = _AuditLogProxyAutoModAction(
automod_rule_name=extra['auto_moderation_rule_name'], automod_rule_name=extra['auto_moderation_rule_name'],
automod_rule_trigger_type=enums.try_enum( automod_rule_trigger_type=enums.try_enum(
enums.AutoModRuleTriggerType, extra['auto_moderation_rule_trigger_type'] enums.AutoModRuleTriggerType, extra['auto_moderation_rule_trigger_type']
), ),
channel=self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id), channel=channel,
) )
elif self.action.name.startswith('overwrite_'): elif self.action.name.startswith('overwrite_'):
@ -723,11 +727,12 @@ class AuditLogEntry(Hashable):
if self.action.target_type is None: if self.action.target_type is None:
return None return None
if self._target_id is None:
return None
try: try:
converter = getattr(self, '_convert_target_' + self.action.target_type) converter = getattr(self, '_convert_target_' + self.action.target_type)
except AttributeError: except AttributeError:
if self._target_id is None:
return None
return Object(id=self._target_id) return Object(id=self._target_id)
else: else:
return converter(self._target_id) return converter(self._target_id)

56
discord/automod.py

@ -25,8 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
import datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, List, Sequence, Set, Union, Sequence from typing import TYPE_CHECKING, Any, Dict, Optional, List, Set, Union, Sequence, overload
from .enums import AutoModRuleTriggerType, AutoModRuleActionType, AutoModRuleEventType, try_enum from .enums import AutoModRuleTriggerType, AutoModRuleActionType, AutoModRuleEventType, try_enum
from .flags import AutoModPresets from .flags import AutoModPresets
@ -59,6 +58,9 @@ __all__ = (
class AutoModRuleAction: class AutoModRuleAction:
"""Represents an auto moderation's rule action. """Represents an auto moderation's rule action.
.. note::
Only one of ``channel_id``, ``duration``, or ``custom_message`` can be used.
.. versionadded:: 2.0 .. versionadded:: 2.0
Attributes Attributes
@ -73,40 +75,65 @@ class AutoModRuleAction:
The duration of the timeout to apply, if any. The duration of the timeout to apply, if any.
Has a maximum of 28 days. Has a maximum of 28 days.
Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.timeout`. Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.timeout`.
custom_message: Optional[:class:`str`]
A custom message which will be shown to a user when their message is blocked.
Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.block_message`.
.. versionadded:: 2.2
""" """
__slots__ = ('type', 'channel_id', 'duration') __slots__ = ('type', 'channel_id', 'duration', 'custom_message')
@overload
def __init__(self, *, channel_id: Optional[int] = ...) -> None:
...
@overload
def __init__(self, *, duration: Optional[datetime.timedelta] = ...) -> None:
...
@overload
def __init__(self, *, custom_message: Optional[str] = ...) -> None:
...
def __init__(self, *, channel_id: Optional[int] = None, duration: Optional[datetime.timedelta] = None) -> None: def __init__(
self,
*,
channel_id: Optional[int] = None,
duration: Optional[datetime.timedelta] = None,
custom_message: Optional[str] = None,
) -> None:
self.channel_id: Optional[int] = channel_id self.channel_id: Optional[int] = channel_id
self.duration: Optional[datetime.timedelta] = duration self.duration: Optional[datetime.timedelta] = duration
if channel_id and duration: self.custom_message: Optional[str] = custom_message
raise ValueError('Please provide only one of ``channel`` or ``duration``')
if sum(v is None for v in (channel_id, duration, custom_message)) < 2:
raise ValueError('Only one of channel_id, duration, or custom_message can be passed.')
self.type: AutoModRuleActionType = AutoModRuleActionType.block_message
if channel_id: if channel_id:
self.type = AutoModRuleActionType.send_alert_message self.type = AutoModRuleActionType.send_alert_message
elif duration: elif duration:
self.type = AutoModRuleActionType.timeout self.type = AutoModRuleActionType.timeout
else:
self.type = AutoModRuleActionType.block_message
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<AutoModRuleAction type={self.type.value} channel={self.channel_id} duration={self.duration}>' return f'<AutoModRuleAction type={self.type.value} channel={self.channel_id} duration={self.duration}>'
@classmethod @classmethod
def from_data(cls, data: AutoModerationActionPayload) -> Self: def from_data(cls, data: AutoModerationActionPayload) -> Self:
type_ = try_enum(AutoModRuleActionType, data['type'])
if data['type'] == AutoModRuleActionType.timeout.value: if data['type'] == AutoModRuleActionType.timeout.value:
duration_seconds = data['metadata']['duration_seconds'] duration_seconds = data['metadata']['duration_seconds']
return cls(duration=datetime.timedelta(seconds=duration_seconds)) return cls(duration=datetime.timedelta(seconds=duration_seconds))
elif data['type'] == AutoModRuleActionType.send_alert_message.value: elif data['type'] == AutoModRuleActionType.send_alert_message.value:
channel_id = int(data['metadata']['channel_id']) channel_id = int(data['metadata']['channel_id'])
return cls(channel_id=channel_id) return cls(channel_id=channel_id)
return cls() return cls(custom_message=data.get('metadata', {}).get('custom_message'))
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
ret = {'type': self.type.value, 'metadata': {}} ret = {'type': self.type.value, 'metadata': {}}
if self.type is AutoModRuleActionType.timeout: if self.type is AutoModRuleActionType.block_message and self.custom_message is not None:
ret['metadata'] = {'custom_message': self.custom_message}
elif self.type is AutoModRuleActionType.timeout:
ret['metadata'] = {'duration_seconds': int(self.duration.total_seconds())} # type: ignore # duration cannot be None here ret['metadata'] = {'duration_seconds': int(self.duration.total_seconds())} # type: ignore # duration cannot be None here
elif self.type is AutoModRuleActionType.send_alert_message: elif self.type is AutoModRuleActionType.send_alert_message:
ret['metadata'] = {'channel_id': str(self.channel_id)} ret['metadata'] = {'channel_id': str(self.channel_id)}
@ -139,13 +166,13 @@ class AutoModTrigger:
The type of trigger. The type of trigger.
keyword_filter: List[:class:`str`] keyword_filter: List[:class:`str`]
The list of strings that will trigger the keyword filter. Maximum of 1000. The list of strings that will trigger the keyword filter. Maximum of 1000.
Keywords can only be up to 30 characters in length. Keywords can only be up to 60 characters in length.
This could be combined with :attr:`regex_patterns`. This could be combined with :attr:`regex_patterns`.
regex_patterns: List[:class:`str`] regex_patterns: List[:class:`str`]
The regex pattern that will trigger the filter. The syntax is based off of The regex pattern that will trigger the filter. The syntax is based off of
`Rust's regex syntax <https://docs.rs/regex/latest/regex/#syntax>`_. `Rust's regex syntax <https://docs.rs/regex/latest/regex/#syntax>`_.
Maximum of 10. Regex strings can only be up to 250 characters in length. Maximum of 10. Regex strings can only be up to 260 characters in length.
This could be combined with :attr:`keyword_filter` and/or :attr:`allow_list` This could be combined with :attr:`keyword_filter` and/or :attr:`allow_list`
@ -153,7 +180,8 @@ class AutoModTrigger:
presets: :class:`AutoModPresets` presets: :class:`AutoModPresets`
The presets used with the preset keyword filter. The presets used with the preset keyword filter.
allow_list: List[:class:`str`] allow_list: List[:class:`str`]
The list of words that are exempt from the commonly flagged words. The list of words that are exempt from the commonly flagged words. Maximum of 100.
Keywords can only be up to 60 characters in length.
mention_limit: :class:`int` mention_limit: :class:`int`
The total number of user and role mentions a message can contain. The total number of user and role mentions a message can contain.
Has a maximum of 50. Has a maximum of 50.

261
discord/channel.py

@ -47,7 +47,7 @@ import datetime
import discord.abc import discord.abc
from .scheduled_event import ScheduledEvent from .scheduled_event import ScheduledEvent
from .permissions import PermissionOverwrite, Permissions from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, ForumLayoutType, PrivacyLevel, try_enum, VideoQualityMode, EntityType from .enums import ChannelType, ForumLayoutType, ForumOrderType, PrivacyLevel, try_enum, VideoQualityMode, EntityType
from .mixins import Hashable from .mixins import Hashable
from . import utils from . import utils
from .utils import MISSING from .utils import MISSING
@ -160,6 +160,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
The default auto archive duration in minutes for threads created in this channel. The default auto archive duration in minutes for threads created in this channel.
.. versionadded:: 2.0 .. versionadded:: 2.0
default_thread_slowmode_delay: :class:`int`
The default slowmode delay in seconds for threads created in this channel.
.. versionadded:: 2.3
""" """
__slots__ = ( __slots__ = (
@ -176,6 +180,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
'_type', '_type',
'last_message_id', 'last_message_id',
'default_auto_archive_duration', 'default_auto_archive_duration',
'default_thread_slowmode_delay',
) )
def __init__(self, *, state: ConnectionState, guild: Guild, data: Union[TextChannelPayload, NewsChannelPayload]): def __init__(self, *, state: ConnectionState, guild: Guild, data: Union[TextChannelPayload, NewsChannelPayload]):
@ -206,6 +211,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
# Does this need coercion into `int`? No idea yet. # Does this need coercion into `int`? No idea yet.
self.slowmode_delay: int = data.get('rate_limit_per_user', 0) 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.default_auto_archive_duration: ThreadArchiveDuration = data.get('default_auto_archive_duration', 1440)
self.default_thread_slowmode_delay: int = data.get('default_thread_rate_limit_per_user', 0)
self._type: Literal[0, 5] = data.get('type', self._type) self._type: Literal[0, 5] = data.get('type', self._type)
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
self._fill_overwrites(data) self._fill_overwrites(data)
@ -301,6 +307,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
category: Optional[CategoryChannel] = ..., category: Optional[CategoryChannel] = ...,
slowmode_delay: int = ..., slowmode_delay: int = ...,
default_auto_archive_duration: ThreadArchiveDuration = ..., default_auto_archive_duration: ThreadArchiveDuration = ...,
default_thread_slowmode_delay: int = ...,
type: ChannelType = ..., type: ChannelType = ...,
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
) -> TextChannel: ) -> TextChannel:
@ -359,7 +366,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
Must be one of ``60``, ``1440``, ``4320``, or ``10080``. Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
.. versionadded:: 2.0 .. versionadded:: 2.0
default_thread_slowmode_delay: :class:`int`
The new default slowmode delay in seconds for threads created in this channel.
.. versionadded:: 2.3
Raises Raises
------ ------
ValueError ValueError
@ -878,7 +888,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
before_timestamp = update_before(threads[-1]) before_timestamp = update_before(threads[-1])
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): class VocalGuildChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
__slots__ = ( __slots__ = (
'name', 'name',
'id', 'id',
@ -901,6 +911,9 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
self.id: int = int(data['id']) self.id: int = int(data['id'])
self._update(guild, data) self._update(guild, data)
async def _get_channel(self) -> Self:
return self
def _get_voice_client_key(self) -> Tuple[int, str]: def _get_voice_client_key(self) -> Tuple[int, str]:
return self.guild.id, 'guild_id' return self.guild.id, 'guild_id'
@ -988,103 +1001,6 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
base.value &= ~denied.value base.value &= ~denied.value
return base return base
class VoiceChannel(discord.abc.Messageable, VocalGuildChannel):
"""Represents a Discord guild voice channel.
.. container:: operations
.. describe:: x == y
Checks if two channels are equal.
.. describe:: x != y
Checks if two channels are not equal.
.. describe:: hash(x)
Returns the channel's hash.
.. describe:: str(x)
Returns the channel's name.
Attributes
-----------
name: :class:`str`
The channel name.
guild: :class:`Guild`
The guild the channel belongs to.
id: :class:`int`
The channel ID.
nsfw: :class:`bool`
If the channel is marked as "not safe for work" or "age restricted".
.. versionadded:: 2.0
category_id: Optional[:class:`int`]
The category channel ID this channel belongs to, if applicable.
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.
bitrate: :class:`int`
The channel's preferred audio bitrate in bits per second.
user_limit: :class:`int`
The channel's limit for number of members that can be in a voice channel.
rtc_region: Optional[:class:`str`]
The region for the voice channel's voice communication.
A value of ``None`` indicates automatic voice region detection.
.. versionadded:: 1.7
.. versionchanged:: 2.0
The type of this attribute has changed to :class:`str`.
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the voice channel's participants.
.. versionadded:: 2.0
last_message_id: Optional[:class:`int`]
The last message ID of the message sent to this channel. It may
*not* point to an existing or valid message.
.. versionadded:: 2.0
slowmode_delay: :class:`int`
The number of seconds a member must wait between sending messages
in this channel. A value of ``0`` denotes that it is disabled.
Bots and users with :attr:`~Permissions.manage_channels` or
:attr:`~Permissions.manage_messages` bypass slowmode.
.. versionadded:: 2.2
"""
__slots__ = ()
def __repr__(self) -> str:
attrs = [
('id', self.id),
('name', self.name),
('rtc_region', self.rtc_region),
('position', self.position),
('bitrate', self.bitrate),
('video_quality_mode', self.video_quality_mode),
('user_limit', self.user_limit),
('category_id', self.category_id),
]
joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
async def _get_channel(self) -> Self:
return self
@property
def _scheduled_event_entity_type(self) -> Optional[EntityType]:
return EntityType.voice
@property
def type(self) -> Literal[ChannelType.voice]:
""":class:`ChannelType`: The channel's Discord type."""
return ChannelType.voice
@property @property
def last_message(self) -> Optional[Message]: def last_message(self) -> Optional[Message]:
"""Retrieves the last message from this channel in cache. """Retrieves the last message from this channel in cache.
@ -1129,7 +1045,7 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel):
from .message import PartialMessage from .message import PartialMessage
return PartialMessage(channel=self, id=message_id) return PartialMessage(channel=self, id=message_id) # type: ignore # VocalGuildChannel is an impl detail
async def delete_messages(self, messages: Iterable[Snowflake], *, reason: Optional[str] = None) -> None: async def delete_messages(self, messages: Iterable[Snowflake], *, reason: Optional[str] = None) -> None:
"""|coro| """|coro|
@ -1332,6 +1248,100 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel):
data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason) data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason)
return Webhook.from_state(data, state=self._state) return Webhook.from_state(data, state=self._state)
class VoiceChannel(VocalGuildChannel):
"""Represents a Discord guild voice channel.
.. container:: operations
.. describe:: x == y
Checks if two channels are equal.
.. describe:: x != y
Checks if two channels are not equal.
.. describe:: hash(x)
Returns the channel's hash.
.. describe:: str(x)
Returns the channel's name.
Attributes
-----------
name: :class:`str`
The channel name.
guild: :class:`Guild`
The guild the channel belongs to.
id: :class:`int`
The channel ID.
nsfw: :class:`bool`
If the channel is marked as "not safe for work" or "age restricted".
.. versionadded:: 2.0
category_id: Optional[:class:`int`]
The category channel ID this channel belongs to, if applicable.
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.
bitrate: :class:`int`
The channel's preferred audio bitrate in bits per second.
user_limit: :class:`int`
The channel's limit for number of members that can be in a voice channel.
rtc_region: Optional[:class:`str`]
The region for the voice channel's voice communication.
A value of ``None`` indicates automatic voice region detection.
.. versionadded:: 1.7
.. versionchanged:: 2.0
The type of this attribute has changed to :class:`str`.
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the voice channel's participants.
.. versionadded:: 2.0
last_message_id: Optional[:class:`int`]
The last message ID of the message sent to this channel. It may
*not* point to an existing or valid message.
.. versionadded:: 2.0
slowmode_delay: :class:`int`
The number of seconds a member must wait between sending messages
in this channel. A value of ``0`` denotes that it is disabled.
Bots and users with :attr:`~Permissions.manage_channels` or
:attr:`~Permissions.manage_messages` bypass slowmode.
.. versionadded:: 2.2
"""
__slots__ = ()
def __repr__(self) -> str:
attrs = [
('id', self.id),
('name', self.name),
('rtc_region', self.rtc_region),
('position', self.position),
('bitrate', self.bitrate),
('video_quality_mode', self.video_quality_mode),
('user_limit', self.user_limit),
('category_id', self.category_id),
]
joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
@property
def _scheduled_event_entity_type(self) -> Optional[EntityType]:
return EntityType.voice
@property
def type(self) -> Literal[ChannelType.voice]:
""":class:`ChannelType`: The channel's Discord type."""
return ChannelType.voice
@utils.copy_doc(discord.abc.GuildChannel.clone) @utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> VoiceChannel: async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> VoiceChannel:
return await self._clone_impl({'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, reason=reason) return await self._clone_impl({'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, reason=reason)
@ -1358,6 +1368,7 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel):
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
rtc_region: Optional[str] = ..., rtc_region: Optional[str] = ...,
video_quality_mode: VideoQualityMode = ..., video_quality_mode: VideoQualityMode = ...,
slowmode_delay: int = ...,
reason: Optional[str] = ..., reason: Optional[str] = ...,
) -> VoiceChannel: ) -> VoiceChannel:
... ...
@ -1492,6 +1503,11 @@ class StageChannel(VocalGuildChannel):
The camera video quality for the stage channel's participants. The camera video quality for the stage channel's participants.
.. versionadded:: 2.0 .. versionadded:: 2.0
last_message_id: Optional[:class:`int`]
The last message ID of the message sent to this channel. It may
*not* point to an existing or valid message.
.. versionadded:: 2.2
slowmode_delay: :class:`int` slowmode_delay: :class:`int`
The number of seconds a member must wait between sending messages The number of seconds a member must wait between sending messages
in this channel. A value of ``0`` denotes that it is disabled. in this channel. A value of ``0`` denotes that it is disabled.
@ -1578,7 +1594,12 @@ class StageChannel(VocalGuildChannel):
return utils.get(self.guild.stage_instances, channel_id=self.id) return utils.get(self.guild.stage_instances, channel_id=self.id)
async def create_instance( async def create_instance(
self, *, topic: str, privacy_level: PrivacyLevel = MISSING, reason: Optional[str] = None self,
*,
topic: str,
privacy_level: PrivacyLevel = MISSING,
send_start_notification: bool = False,
reason: Optional[str] = None,
) -> StageInstance: ) -> StageInstance:
"""|coro| """|coro|
@ -1594,6 +1615,11 @@ class StageChannel(VocalGuildChannel):
The stage instance's topic. The stage instance's topic.
privacy_level: :class:`PrivacyLevel` privacy_level: :class:`PrivacyLevel`
The stage instance's privacy level. Defaults to :attr:`PrivacyLevel.guild_only`. The stage instance's privacy level. Defaults to :attr:`PrivacyLevel.guild_only`.
send_start_notification: :class:`bool`
Whether to send a start notification. This sends a push notification to @everyone if ``True``. Defaults to ``False``.
You must have :attr:`~Permissions.mention_everyone` to do this.
.. versionadded:: 2.3
reason: :class:`str` reason: :class:`str`
The reason the stage instance was created. Shows up on the audit log. The reason the stage instance was created. Shows up on the audit log.
@ -1620,6 +1646,8 @@ class StageChannel(VocalGuildChannel):
payload['privacy_level'] = privacy_level.value payload['privacy_level'] = privacy_level.value
payload['send_start_notification'] = send_start_notification
data = await self._state.http.create_stage_instance(**payload, reason=reason) data = await self._state.http.create_stage_instance(**payload, reason=reason)
return StageInstance(guild=self.guild, state=self._state, data=data) return StageInstance(guild=self.guild, state=self._state, data=data)
@ -1665,6 +1693,7 @@ class StageChannel(VocalGuildChannel):
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
rtc_region: Optional[str] = ..., rtc_region: Optional[str] = ...,
video_quality_mode: VideoQualityMode = ..., video_quality_mode: VideoQualityMode = ...,
slowmode_delay: int = ...,
reason: Optional[str] = ..., reason: Optional[str] = ...,
) -> StageChannel: ) -> StageChannel:
... ...
@ -2147,6 +2176,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
Defaults to :attr:`ForumLayoutType.not_set`. Defaults to :attr:`ForumLayoutType.not_set`.
.. versionadded:: 2.2 .. versionadded:: 2.2
default_sort_order: Optional[:class:`ForumOrderType`]
The default sort order for posts in this forum channel.
.. versionadded:: 2.3
""" """
__slots__ = ( __slots__ = (
@ -2166,6 +2199,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
'default_thread_slowmode_delay', 'default_thread_slowmode_delay',
'default_reaction_emoji', 'default_reaction_emoji',
'default_layout', 'default_layout',
'default_sort_order',
'_available_tags', '_available_tags',
'_flags', '_flags',
) )
@ -2211,6 +2245,11 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
name=default_reaction_emoji.get('emoji_name') or '', name=default_reaction_emoji.get('emoji_name') or '',
) )
self.default_sort_order: Optional[ForumOrderType] = None
default_sort_order = data.get('default_sort_order')
if default_sort_order is not None:
self.default_sort_order = try_enum(ForumOrderType, default_sort_order)
self._flags: int = data.get('flags', 0) self._flags: int = data.get('flags', 0)
self._fill_overwrites(data) self._fill_overwrites(data)
@ -2337,6 +2376,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
default_thread_slowmode_delay: int = ..., default_thread_slowmode_delay: int = ...,
default_reaction_emoji: Optional[EmojiInputType] = ..., default_reaction_emoji: Optional[EmojiInputType] = ...,
default_layout: ForumLayoutType = ..., default_layout: ForumLayoutType = ...,
default_sort_order: ForumOrderType = ...,
require_tag: bool = ..., require_tag: bool = ...,
) -> ForumChannel: ) -> ForumChannel:
... ...
@ -2395,6 +2435,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
The new default layout for posts in this forum. The new default layout for posts in this forum.
.. versionadded:: 2.2 .. versionadded:: 2.2
default_sort_order: Optional[:class:`ForumOrderType`]
The new default sort order for posts in this forum.
.. versionadded:: 2.3
require_tag: :class:`bool` require_tag: :class:`bool`
Whether to require a tag for threads in this channel or not. Whether to require a tag for threads in this channel or not.
@ -2457,6 +2501,21 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
options['default_forum_layout'] = layout.value options['default_forum_layout'] = layout.value
try:
sort_order = options.pop('default_sort_order')
except KeyError:
pass
else:
if sort_order is None:
options['default_sort_order'] = None
else:
if not isinstance(sort_order, ForumOrderType):
raise TypeError(
f'default_sort_order parameter must be a ForumOrderType not {sort_order.__class__.__name__}'
)
options['default_sort_order'] = sort_order.value
payload = await self._edit(options, reason=reason) payload = await self._edit(options, reason=reason)
if payload is not None: if payload is not None:
# the payload will always be the proper channel payload # the payload will always be the proper channel payload

14
discord/client.py

@ -1161,9 +1161,9 @@ class Client:
event: Literal['app_command_completion'], event: Literal['app_command_completion'],
/, /,
*, *,
check: Optional[Callable[[Interaction[Self], Union[Command, ContextMenu]], bool]], check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]],
timeout: Optional[float] = None, timeout: Optional[float] = None,
) -> Tuple[Interaction[Self], Union[Command, ContextMenu]]: ) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]:
... ...
# AutoMod # AutoMod
@ -1816,9 +1816,9 @@ class Client:
event: Literal["command", "command_completion"], event: Literal["command", "command_completion"],
/, /,
*, *,
check: Optional[Callable[[Context], bool]] = None, check: Optional[Callable[[Context[Any]], bool]] = None,
timeout: Optional[float] = None, timeout: Optional[float] = None,
) -> Context: ) -> Context[Any]:
... ...
@overload @overload
@ -1827,9 +1827,9 @@ class Client:
event: Literal["command_error"], event: Literal["command_error"],
/, /,
*, *,
check: Optional[Callable[[Context, CommandError], bool]] = None, check: Optional[Callable[[Context[Any], CommandError], bool]] = None,
timeout: Optional[float] = None, timeout: Optional[float] = None,
) -> Tuple[Context, CommandError]: ) -> Tuple[Context[Any], CommandError]:
... ...
@overload @overload
@ -2486,8 +2486,6 @@ class Client:
The bot's application information. The bot's application information.
""" """
data = await self.http.application_info() data = await self.http.application_info()
if 'rpc_origins' not in data:
data['rpc_origins'] = None
return AppInfo(self._connection, data) return AppInfo(self._connection, data)
async def fetch_user(self, user_id: int, /) -> User: async def fetch_user(self, user_id: int, /) -> User:

5
discord/colour.py

@ -104,6 +104,11 @@ class Colour:
Returns the raw colour value. Returns the raw colour value.
.. note::
The colour values in the classmethods are mostly provided as-is and can change between
versions should the Discord client's representation of that colour also change.
Attributes Attributes
------------ ------------
value: :class:`int` value: :class:`int`

16
discord/enums.py

@ -67,6 +67,7 @@ __all__ = (
'AutoModRuleEventType', 'AutoModRuleEventType',
'AutoModRuleActionType', 'AutoModRuleActionType',
'ForumLayoutType', 'ForumLayoutType',
'ForumOrderType',
'OnboardingPromptType', 'OnboardingPromptType',
) )
@ -235,11 +236,11 @@ class MessageType(Enum):
auto_moderation_action = 24 auto_moderation_action = 24
role_subscription_purchase = 25 role_subscription_purchase = 25
interaction_premium_upsell = 26 interaction_premium_upsell = 26
# stage_start = 27 stage_start = 27
# stage_end = 28 stage_end = 28
# stage_speaker = 29 stage_speaker = 29
# stage_raise_hand = 30 stage_raise_hand = 30
# stage_topic = 31 stage_topic = 31
guild_application_premium_subscription = 32 guild_application_premium_subscription = 32
@ -752,6 +753,11 @@ class ForumLayoutType(Enum):
gallery_view = 2 gallery_view = 2
class ForumOrderType(Enum):
latest_activity = 0
creation_date = 1
class OnboardingPromptType(Enum): class OnboardingPromptType(Enum):
multiple_choice = 0 multiple_choice = 0
dropdown = 1 dropdown = 1

13
discord/ext/commands/cog.py

@ -50,6 +50,7 @@ from ._types import _BaseCommand, BotT
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from discord.abc import Snowflake from discord.abc import Snowflake
from discord._types import ClientT
from .bot import BotBase from .bot import BotBase
from .context import Context from .context import Context
@ -585,6 +586,18 @@ class Cog(metaclass=CogMeta):
""" """
return True return True
@_cog_special_method
def interaction_check(self, interaction: discord.Interaction[ClientT], /) -> bool:
"""A special method that registers as a :func:`discord.app_commands.check`
for every app command and subcommand in this cog.
This function **can** be a coroutine and must take a sole parameter,
``interaction``, to represent the :class:`~discord.Interaction`.
.. versionadded:: 2.0
"""
return True
@_cog_special_method @_cog_special_method
async def cog_command_error(self, ctx: Context[BotT], error: Exception) -> None: async def cog_command_error(self, ctx: Context[BotT], error: Exception) -> None:
"""|coro| """|coro|

170
discord/ext/commands/context.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
import re import re
from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, TypeVar, Union, Sequence, Type from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, TypeVar, Union, Sequence, Type, overload
import discord.abc import discord.abc
import discord.utils import discord.utils
@ -615,6 +615,90 @@ class Context(discord.abc.Messageable, Generic[BotT]):
except CommandError as e: except CommandError as e:
await cmd.on_help_command_error(self, e) await cmd.on_help_command_error(self, e)
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
"""|coro| """|coro|
@ -716,6 +800,90 @@ class Context(discord.abc.Messageable, Generic[BotT]):
if self.interaction: if self.interaction:
await self.interaction.response.defer(ephemeral=ephemeral) await self.interaction.response.defer(ephemeral=ephemeral)
@overload
async def send(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def send(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def send(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def send(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
ephemeral: bool = ...,
silent: bool = ...,
) -> Message:
...
async def send( async def send(
self, self,
content: Optional[str] = None, content: Optional[str] = None,

2
discord/ext/commands/converter.py

@ -1330,7 +1330,7 @@ async def run_converters(ctx: Context[BotT], converter: Any, argument: str, para
return value return value
# if we're here, then we failed to match all the literals # if we're here, then we failed to match all the literals
raise BadLiteralArgument(param, literal_args, errors) raise BadLiteralArgument(param, literal_args, errors, argument)
# This must be the last if-clause in the chain of origin checking # This must be the last if-clause in the chain of origin checking
# Nearly every type is a generic type within the typing library # Nearly every type is a generic type within the typing library

7
discord/ext/commands/errors.py

@ -920,12 +920,17 @@ class BadLiteralArgument(UserInputError):
A tuple of values compared against in conversion, in order of failure. A tuple of values compared against in conversion, in order of failure.
errors: List[:class:`CommandError`] errors: List[:class:`CommandError`]
A list of errors that were caught from failing the conversion. A list of errors that were caught from failing the conversion.
argument: :class:`str`
The argument's value that failed to be converted. Defaults to an empty string.
.. versionadded:: 2.3
""" """
def __init__(self, param: Parameter, literals: Tuple[Any, ...], errors: List[CommandError]) -> None: def __init__(self, param: Parameter, literals: Tuple[Any, ...], errors: List[CommandError], argument: str = "") -> None:
self.param: Parameter = param self.param: Parameter = param
self.literals: Tuple[Any, ...] = literals self.literals: Tuple[Any, ...] = literals
self.errors: List[CommandError] = errors self.errors: List[CommandError] = errors
self.argument: str = argument
to_string = [repr(l) for l in literals] to_string = [repr(l) for l in literals]
if len(to_string) > 2: if len(to_string) > 2:

11
discord/ext/commands/hybrid.py

@ -72,9 +72,9 @@ __all__ = (
T = TypeVar('T') T = TypeVar('T')
U = TypeVar('U') U = TypeVar('U')
CogT = TypeVar('CogT', bound='Cog') CogT = TypeVar('CogT', bound='Cog')
CommandT = TypeVar('CommandT', bound='Command') CommandT = TypeVar('CommandT', bound='Command[Any, ..., Any]')
# CHT = TypeVar('CHT', bound='Check') # CHT = TypeVar('CHT', bound='Check')
GroupT = TypeVar('GroupT', bound='Group') GroupT = TypeVar('GroupT', bound='Group[Any, ..., Any]')
_NoneType = type(None) _NoneType = type(None)
if TYPE_CHECKING: if TYPE_CHECKING:
@ -297,7 +297,7 @@ def replace_parameters(
class HybridAppCommand(discord.app_commands.Command[CogT, P, T]): class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
def __init__(self, wrapped: Union[HybridCommand[CogT, Any, T], HybridGroup[CogT, Any, T]]) -> None: def __init__(self, wrapped: Union[HybridCommand[CogT, ..., T], HybridGroup[CogT, ..., T]]) -> None:
signature = inspect.signature(wrapped.callback) signature = inspect.signature(wrapped.callback)
params = replace_parameters(wrapped.params, wrapped.callback, signature) params = replace_parameters(wrapped.params, wrapped.callback, signature)
wrapped.callback.__signature__ = signature.replace(parameters=params) wrapped.callback.__signature__ = signature.replace(parameters=params)
@ -312,7 +312,7 @@ class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
finally: finally:
del wrapped.callback.__signature__ del wrapped.callback.__signature__
self.wrapped: Union[HybridCommand[CogT, Any, T], HybridGroup[CogT, Any, T]] = wrapped self.wrapped: Union[HybridCommand[CogT, ..., T], HybridGroup[CogT, ..., T]] = wrapped
self.binding: Optional[CogT] = wrapped.cog self.binding: Optional[CogT] = wrapped.cog
# This technically means only one flag converter is supported # This technically means only one flag converter is supported
self.flag_converter: Optional[Tuple[str, Type[FlagConverter]]] = getattr( self.flag_converter: Optional[Tuple[str, Type[FlagConverter]]] = getattr(
@ -908,6 +908,9 @@ def hybrid_group(
Parameters Parameters
----------- -----------
name: Union[:class:`str`, :class:`~discord.app_commands.locale_str`]
The name to create the group with. By default this uses the
function name unchanged.
with_app_command: :class:`bool` with_app_command: :class:`bool`
Whether to register the command also as an application command. Whether to register the command also as an application command.

15
discord/gateway.py

@ -915,6 +915,7 @@ class DiscordVoiceWebSocket:
'd': { 'd': {
'speaking': int(state), 'speaking': int(state),
'delay': 0, 'delay': 0,
'ssrc': self._connection.ssrc,
}, },
} }
@ -948,16 +949,16 @@ class DiscordVoiceWebSocket:
state.voice_port = data['port'] state.voice_port = data['port']
state.endpoint_ip = data['ip'] state.endpoint_ip = data['ip']
packet = bytearray(70) packet = bytearray(74)
struct.pack_into('>H', packet, 0, 1) # 1 = Send struct.pack_into('>H', packet, 0, 1) # 1 = Send
struct.pack_into('>H', packet, 2, 70) # 70 = Length struct.pack_into('>H', packet, 2, 70) # 70 = Length
struct.pack_into('>I', packet, 4, state.ssrc) struct.pack_into('>I', packet, 4, state.ssrc)
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
recv = await self.loop.sock_recv(state.socket, 70) recv = await self.loop.sock_recv(state.socket, 74)
_log.debug('received packet in initial_connection: %s', recv) _log.debug('received packet in initial_connection: %s', recv)
# the ip is ascii starting at the 4th byte and ending at the first null # the ip is ascii starting at the 8th byte and ending at the first null
ip_start = 4 ip_start = 8
ip_end = recv.index(0, ip_start) ip_end = recv.index(0, ip_start)
state.ip = recv[ip_start:ip_end].decode('ascii') state.ip = recv[ip_start:ip_end].decode('ascii')
@ -990,7 +991,11 @@ class DiscordVoiceWebSocket:
async def load_secret_key(self, data: Dict[str, Any]) -> None: async def load_secret_key(self, data: Dict[str, Any]) -> None:
_log.debug('received secret key for voice connection') _log.debug('received secret key for voice connection')
self.secret_key = self._connection.secret_key = data['secret_key'] self.secret_key = self._connection.secret_key = data['secret_key']
await self.speak()
# Send a speak command with the "not speaking" state.
# This also tells Discord our SSRC value, which Discord requires
# before sending any voice data (and is the real reason why we
# call this here).
await self.speak(SpeakingState.none) await self.speak(SpeakingState.none)
async def poll_event(self) -> None: async def poll_event(self) -> None:

300
discord/guild.py

@ -73,6 +73,8 @@ from .enums import (
MFALevel, MFALevel,
Locale, Locale,
AutoModRuleEventType, AutoModRuleEventType,
ForumOrderType,
ForumLayoutType,
) )
from .mixins import Hashable from .mixins import Hashable
from .user import User from .user import User
@ -91,6 +93,7 @@ from .object import OLDEST_OBJECT, Object
from .onboarding import Onboarding from .onboarding import Onboarding
from .welcome_screen import WelcomeScreen, WelcomeChannel from .welcome_screen import WelcomeScreen, WelcomeChannel
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
from .partial_emoji import _EmojiTag, PartialEmoji
__all__ = ( __all__ = (
@ -130,6 +133,7 @@ if TYPE_CHECKING:
from .types.integration import IntegrationType from .types.integration import IntegrationType
from .types.snowflake import SnowflakeList from .types.snowflake import SnowflakeList
from .types.widget import EditWidgetSettings from .types.widget import EditWidgetSettings
from .message import EmojiInputType
VocalGuildChannel = Union[VoiceChannel, StageChannel] VocalGuildChannel = Union[VoiceChannel, StageChannel]
GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel]
@ -290,6 +294,7 @@ class Guild(Hashable):
'mfa_level', 'mfa_level',
'vanity_url_code', 'vanity_url_code',
'widget_enabled', 'widget_enabled',
'_widget_channel_id',
'_members', '_members',
'_channels', '_channels',
'_icon', '_icon',
@ -478,6 +483,7 @@ class Guild(Hashable):
self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0 self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0
self.vanity_url_code: Optional[str] = guild.get('vanity_url_code') self.vanity_url_code: Optional[str] = guild.get('vanity_url_code')
self.widget_enabled: bool = guild.get('widget_enabled', False) self.widget_enabled: bool = guild.get('widget_enabled', False)
self._widget_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'widget_channel_id')
self._system_channel_flags: int = guild.get('system_channel_flags', 0) self._system_channel_flags: int = guild.get('system_channel_flags', 0)
self.preferred_locale: Locale = try_enum(Locale, guild.get('preferred_locale', 'en-US')) self.preferred_locale: Locale = try_enum(Locale, guild.get('preferred_locale', 'en-US'))
self._discovery_splash: Optional[str] = guild.get('discovery_splash') self._discovery_splash: Optional[str] = guild.get('discovery_splash')
@ -737,6 +743,26 @@ class Guild(Hashable):
""" """
return self._threads.get(thread_id) return self._threads.get(thread_id)
def get_emoji(self, emoji_id: int, /) -> Optional[Emoji]:
"""Returns an emoji with the given ID.
.. versionadded:: 2.3
Parameters
----------
emoji_id: int
The ID to search for.
Returns
--------
Optional[:class:`Emoji`]
The returned Emoji or ``None`` if not found.
"""
emoji = self._state.get_emoji(emoji_id)
if emoji and emoji.guild == self:
return emoji
return None
@property @property
def system_channel(self) -> Optional[TextChannel]: def system_channel(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages. """Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages.
@ -776,6 +802,18 @@ class Guild(Hashable):
channel_id = self._public_updates_channel_id channel_id = self._public_updates_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore return channel_id and self._channels.get(channel_id) # type: ignore
@property
def widget_channel(self) -> Optional[Union[TextChannel, ForumChannel, VoiceChannel, StageChannel]]:
"""Optional[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`, :class:`StageChannel`]]: Returns
the widget channel of the guild.
If no channel is set, then this returns ``None``.
.. versionadded:: 2.3
"""
channel_id = self._widget_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore
@property @property
def emoji_limit(self) -> int: def emoji_limit(self) -> int:
""":class:`int`: The maximum number of emoji slots this guild has.""" """:class:`int`: The maximum number of emoji slots this guild has."""
@ -1200,6 +1238,7 @@ class Guild(Hashable):
nsfw: bool = MISSING, nsfw: bool = MISSING,
overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING,
default_auto_archive_duration: int = MISSING, default_auto_archive_duration: int = MISSING,
default_thread_slowmode_delay: int = MISSING,
) -> TextChannel: ) -> TextChannel:
"""|coro| """|coro|
@ -1273,6 +1312,10 @@ class Guild(Hashable):
Must be one of ``60``, ``1440``, ``4320``, or ``10080``. Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
.. versionadded:: 2.0 .. versionadded:: 2.0
default_thread_slowmode_delay: :class:`int`
The default slowmode delay in seconds for threads created in the text channel.
.. versionadded:: 2.3
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for creating this channel. Shows up on the audit log. The reason for creating this channel. Shows up on the audit log.
@ -1305,7 +1348,10 @@ class Guild(Hashable):
options['nsfw'] = nsfw options['nsfw'] = nsfw
if default_auto_archive_duration is not MISSING: if default_auto_archive_duration is not MISSING:
options["default_auto_archive_duration"] = default_auto_archive_duration options['default_auto_archive_duration'] = default_auto_archive_duration
if default_thread_slowmode_delay is not MISSING:
options['default_thread_rate_limit_per_user'] = default_thread_slowmode_delay
data = await self._create_channel( data = await self._create_channel(
name, name,
@ -1577,6 +1623,9 @@ class Guild(Hashable):
reason: Optional[str] = None, reason: Optional[str] = None,
default_auto_archive_duration: int = MISSING, default_auto_archive_duration: int = MISSING,
default_thread_slowmode_delay: int = MISSING, default_thread_slowmode_delay: int = MISSING,
default_sort_order: ForumOrderType = MISSING,
default_reaction_emoji: EmojiInputType = MISSING,
default_layout: ForumLayoutType = MISSING,
available_tags: Sequence[ForumTag] = MISSING, available_tags: Sequence[ForumTag] = MISSING,
) -> ForumChannel: ) -> ForumChannel:
"""|coro| """|coro|
@ -1594,6 +1643,10 @@ class Guild(Hashable):
----------- -----------
name: :class:`str` name: :class:`str`
The channel's name. The channel's name.
overwrites: Dict[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`]
A :class:`dict` of target (either a role or a member) to
:class:`PermissionOverwrite` to apply upon creation of a channel.
Useful for creating secret channels.
topic: :class:`str` topic: :class:`str`
The channel's topic. The channel's topic.
category: Optional[:class:`CategoryChannel`] category: Optional[:class:`CategoryChannel`]
@ -1617,6 +1670,19 @@ class Guild(Hashable):
The default slowmode delay in seconds for threads created in this forum. The default slowmode delay in seconds for threads created in this forum.
.. versionadded:: 2.1 .. versionadded:: 2.1
default_sort_order: :class:`ForumOrderType`
The default sort order for posts in this forum channel.
.. versionadded:: 2.3
default_reaction_emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]
The default reaction emoji for threads created in this forum to show in the
add reaction button.
.. versionadded:: 2.3
default_layout: :class:`ForumLayoutType`
The default layout for posts in this forum.
.. versionadded:: 2.3
available_tags: Sequence[:class:`ForumTag`] available_tags: Sequence[:class:`ForumTag`]
The available tags for this forum channel. The available tags for this forum channel.
@ -1656,6 +1722,30 @@ class Guild(Hashable):
if default_thread_slowmode_delay is not MISSING: if default_thread_slowmode_delay is not MISSING:
options['default_thread_rate_limit_per_user'] = default_thread_slowmode_delay options['default_thread_rate_limit_per_user'] = default_thread_slowmode_delay
if default_sort_order is not MISSING:
if not isinstance(default_sort_order, ForumOrderType):
raise TypeError(
f'default_sort_order parameter must be a ForumOrderType not {default_sort_order.__class__.__name__}'
)
options['default_sort_order'] = default_sort_order.value
if default_reaction_emoji is not MISSING:
if isinstance(default_reaction_emoji, _EmojiTag):
options['default_reaction_emoji'] = default_reaction_emoji._to_partial()._to_forum_tag_payload()
elif isinstance(default_reaction_emoji, str):
options['default_reaction_emoji'] = PartialEmoji.from_str(default_reaction_emoji)._to_forum_tag_payload()
else:
raise ValueError(f'default_reaction_emoji parameter must be either Emoji, PartialEmoji, or str')
if default_layout is not MISSING:
if not isinstance(default_layout, ForumLayoutType):
raise TypeError(
f'default_layout parameter must be a ForumLayoutType not {default_layout.__class__.__name__}'
)
options['default_forum_layout'] = default_layout.value
if available_tags is not MISSING: if available_tags is not MISSING:
options['available_tags'] = [t.to_dict() for t in available_tags] options['available_tags'] = [t.to_dict() for t in available_tags]
@ -1728,6 +1818,9 @@ class Guild(Hashable):
premium_progress_bar_enabled: bool = MISSING, premium_progress_bar_enabled: bool = MISSING,
discoverable: bool = MISSING, discoverable: bool = MISSING,
invites_disabled: bool = MISSING, invites_disabled: bool = MISSING,
widget_enabled: bool = MISSING,
widget_channel: Optional[Snowflake] = MISSING,
mfa_level: MFALevel = MISSING,
) -> Guild: ) -> Guild:
r"""|coro| r"""|coro|
@ -1735,12 +1828,6 @@ class Guild(Hashable):
You must have :attr:`~Permissions.manage_guild` to edit the guild. You must have :attr:`~Permissions.manage_guild` to edit the guild.
.. versionchanged:: 1.4
The ``rules_channel`` and ``public_updates_channel`` keyword parameters were added.
.. versionchanged:: 2.0
The ``discovery_splash`` and ``community`` keyword parameters were added.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
The newly updated guild is returned. The newly updated guild is returned.
@ -1751,15 +1838,6 @@ class Guild(Hashable):
This function will now raise :exc:`TypeError` or This function will now raise :exc:`TypeError` or
:exc:`ValueError` instead of ``InvalidArgument``. :exc:`ValueError` instead of ``InvalidArgument``.
.. versionchanged:: 2.0
The ``preferred_locale`` keyword parameter now accepts an enum instead of :class:`str`.
.. versionchanged:: 2.0
The ``premium_progress_bar_enabled`` keyword parameter was added.
.. versionchanged:: 2.1
The ``discoverable`` and ``invites_disabled`` keyword parameters were added.
Parameters Parameters
---------- ----------
name: :class:`str` name: :class:`str`
@ -1785,9 +1863,13 @@ class Guild(Hashable):
Only PNG/JPEG supported. Could be ``None`` to denote removing the Only PNG/JPEG supported. Could be ``None`` to denote removing the
splash. This is only available to guilds that contain ``DISCOVERABLE`` splash. This is only available to guilds that contain ``DISCOVERABLE``
in :attr:`Guild.features`. in :attr:`Guild.features`.
.. versionadded:: 2.0
community: :class:`bool` community: :class:`bool`
Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel`` Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel``
and ``public_updates_channel`` parameters are required. and ``public_updates_channel`` parameters are required.
.. versionadded:: 2.0
afk_channel: Optional[:class:`VoiceChannel`] afk_channel: Optional[:class:`VoiceChannel`]
The new channel that is the AFK channel. Could be ``None`` for no AFK channel. The new channel that is the AFK channel. Could be ``None`` for no AFK channel.
afk_timeout: :class:`int` afk_timeout: :class:`int`
@ -1809,20 +1891,47 @@ class Guild(Hashable):
The new system channel settings to use with the new system channel. The new system channel settings to use with the new system channel.
preferred_locale: :class:`Locale` preferred_locale: :class:`Locale`
The new preferred locale for the guild. Used as the primary language in the guild. The new preferred locale for the guild. Used as the primary language in the guild.
.. versionchanged:: 2.0
Now accepts an enum instead of :class:`str`.
rules_channel: Optional[:class:`TextChannel`] rules_channel: Optional[:class:`TextChannel`]
The new channel that is used for rules. This is only available to The new channel that is used for rules. This is only available to
guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no rules guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no rules
channel. channel.
.. versionadded:: 1.4
public_updates_channel: Optional[:class:`TextChannel`] public_updates_channel: Optional[:class:`TextChannel`]
The new channel that is used for public updates from Discord. This is only available to The new channel that is used for public updates from Discord. This is only available to
guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no
public updates channel. public updates channel.
.. versionadded:: 1.4
premium_progress_bar_enabled: :class:`bool` premium_progress_bar_enabled: :class:`bool`
Whether the premium AKA server boost level progress bar should be enabled for the guild. Whether the premium AKA server boost level progress bar should be enabled for the guild.
.. versionadded:: 2.0
discoverable: :class:`bool` discoverable: :class:`bool`
Whether server discovery is enabled for this guild. Whether server discovery is enabled for this guild.
.. versionadded:: 2.1
invites_disabled: :class:`bool` invites_disabled: :class:`bool`
Whether joining via invites should be disabled for the guild. Whether joining via invites should be disabled for the guild.
.. versionadded:: 2.1
widget_enabled: :class:`bool`
Whether to enable the widget for the guild.
.. versionadded:: 2.3
widget_channel: Optional[:class:`abc.Snowflake`]
The new widget channel. ``None`` removes the widget channel.
.. versionadded:: 2.3
mfa_level: :class:`MFALevel`
The new guild's Multi-Factor Authentication requirement level.
Note that you must be owner of the guild to do this.
.. versionadded:: 2.3
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for editing this guild. Shows up on the audit log. The reason for editing this guild. Shows up on the audit log.
@ -1838,7 +1947,7 @@ class Guild(Hashable):
guild and request an ownership transfer. guild and request an ownership transfer.
TypeError TypeError
The type passed to the ``default_notifications``, ``verification_level``, The type passed to the ``default_notifications``, ``verification_level``,
``explicit_content_filter``, or ``system_channel_flags`` parameter was ``explicit_content_filter``, ``system_channel_flags``, or ``mfa_level`` parameter was
of the incorrect type. of the incorrect type.
Returns Returns
@ -1974,6 +2083,21 @@ class Guild(Hashable):
if premium_progress_bar_enabled is not MISSING: if premium_progress_bar_enabled is not MISSING:
fields['premium_progress_bar_enabled'] = premium_progress_bar_enabled fields['premium_progress_bar_enabled'] = premium_progress_bar_enabled
widget_payload: EditWidgetSettings = {}
if widget_channel is not MISSING:
widget_payload['channel_id'] = None if widget_channel is None else widget_channel.id
if widget_enabled is not MISSING:
widget_payload['enabled'] = widget_enabled
if widget_payload:
await self._state.http.edit_widget(self.id, payload=widget_payload, reason=reason)
if mfa_level is not MISSING:
if not isinstance(mfa_level, MFALevel):
raise TypeError(f'mfa_level must be of type MFALevel not {mfa_level.__class__.__name__}')
await http.edit_guild_mfa_level(self.id, mfa_level=mfa_level.value)
data = await http.edit_guild(self.id, reason=reason, **fields) data = await http.edit_guild(self.id, reason=reason, **fields)
return Guild(data=data, state=self._state) return Guild(data=data, state=self._state)
@ -2783,7 +2907,7 @@ class Guild(Hashable):
Parameters Parameters
------------ ------------
id: :class:`int` scheduled_event_id: :class:`int`
The scheduled event ID. The scheduled event ID.
with_counts: :class:`bool` with_counts: :class:`bool`
Whether to include the number of users that are subscribed to the event. Whether to include the number of users that are subscribed to the event.
@ -2804,6 +2928,68 @@ class Guild(Hashable):
data = await self._state.http.get_scheduled_event(self.id, scheduled_event_id, with_counts) data = await self._state.http.get_scheduled_event(self.id, scheduled_event_id, with_counts)
return ScheduledEvent(state=self._state, data=data) return ScheduledEvent(state=self._state, data=data)
@overload
async def create_scheduled_event(
self,
*,
name: str,
start_time: datetime.datetime,
entity_type: Literal[EntityType.external] = ...,
privacy_level: PrivacyLevel = ...,
location: str = ...,
end_time: datetime.datetime = ...,
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def create_scheduled_event(
self,
*,
name: str,
start_time: datetime.datetime,
entity_type: Literal[EntityType.stage_instance, EntityType.voice] = ...,
privacy_level: PrivacyLevel = ...,
channel: Snowflake = ...,
end_time: datetime.datetime = ...,
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def create_scheduled_event(
self,
*,
name: str,
start_time: datetime.datetime,
privacy_level: PrivacyLevel = ...,
location: str = ...,
end_time: datetime.datetime = ...,
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def create_scheduled_event(
self,
*,
name: str,
start_time: datetime.datetime,
privacy_level: PrivacyLevel = ...,
channel: Union[VoiceChannel, StageChannel] = ...,
end_time: datetime.datetime = ...,
description: str = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
async def create_scheduled_event( async def create_scheduled_event(
self, self,
*, *,
@ -2847,6 +3033,8 @@ class Guild(Hashable):
datetime object. Consider using :func:`utils.utcnow`. datetime object. Consider using :func:`utils.utcnow`.
Required if the entity type is :attr:`EntityType.external`. Required if the entity type is :attr:`EntityType.external`.
privacy_level: :class:`PrivacyLevel`
The privacy level of the scheduled event.
entity_type: :class:`EntityType` entity_type: :class:`EntityType`
The entity type of the scheduled event. If the channel is a The entity type of the scheduled event. If the channel is a
:class:`StageInstance` or :class:`VoiceChannel` then this is :class:`StageInstance` or :class:`VoiceChannel` then this is
@ -2894,27 +3082,32 @@ class Guild(Hashable):
) )
payload['scheduled_start_time'] = start_time.isoformat() payload['scheduled_start_time'] = start_time.isoformat()
entity_type = entity_type or getattr(channel, '_scheduled_event_entity_type', MISSING)
if entity_type is MISSING: if entity_type is MISSING:
if channel is MISSING: if channel and isinstance(channel, Object):
if channel.type is VoiceChannel:
entity_type = EntityType.voice
elif channel.type is StageChannel:
entity_type = EntityType.stage_instance
elif location not in (MISSING, None):
entity_type = EntityType.external entity_type = EntityType.external
else: else:
_entity_type = getattr(channel, '_scheduled_event_entity_type', MISSING) if not isinstance(entity_type, EntityType):
if _entity_type is None: raise TypeError('entity_type must be of type EntityType')
raise TypeError(
'invalid GuildChannel type passed, must be VoiceChannel or StageChannel '
f'not {channel.__class__.__name__}'
)
if _entity_type is MISSING:
raise TypeError('entity_type must be passed in when passing an ambiguous channel type')
entity_type = _entity_type payload['entity_type'] = entity_type.value
if not isinstance(entity_type, EntityType): if entity_type is None:
raise TypeError('entity_type must be of type EntityType') raise TypeError(
'invalid GuildChannel type passed, must be VoiceChannel or StageChannel ' f'not {channel.__class__.__name__}'
)
payload['entity_type'] = entity_type.value if privacy_level is not MISSING:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level must be of type PrivacyLevel.')
payload['privacy_level'] = PrivacyLevel.guild_only.value payload['privacy_level'] = privacy_level.value
if description is not MISSING: if description is not MISSING:
payload['description'] = description payload['description'] = description
@ -2924,7 +3117,7 @@ class Guild(Hashable):
payload['image'] = image_as_str payload['image'] = image_as_str
if entity_type in (EntityType.stage_instance, EntityType.voice): if entity_type in (EntityType.stage_instance, EntityType.voice):
if channel is MISSING or channel is None: if channel in (MISSING, None):
raise TypeError('channel must be set when entity_type is voice or stage_instance') raise TypeError('channel must be set when entity_type is voice or stage_instance')
payload['channel_id'] = channel.id payload['channel_id'] = channel.id
@ -2940,12 +3133,15 @@ class Guild(Hashable):
metadata['location'] = location metadata['location'] = location
if end_time is not MISSING: if end_time in (MISSING, None):
if end_time.tzinfo is None: raise TypeError('end_time must be set when entity_type is external')
raise ValueError(
'end_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' if end_time not in (MISSING, None):
) if end_time.tzinfo is None:
payload['scheduled_end_time'] = end_time.isoformat() raise ValueError(
'end_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
)
payload['scheduled_end_time'] = end_time.isoformat()
if metadata: if metadata:
payload['entity_metadata'] = metadata payload['entity_metadata'] = metadata
@ -3631,7 +3827,7 @@ class Guild(Hashable):
if limit is not None: if limit is not None:
limit -= len(entries) limit -= len(entries)
after = Object(id=int(entries[0]['id'])) after = Object(id=int(entries[-1]['id']))
return data, entries, after, limit return data, entries, after, limit
@ -3648,20 +3844,19 @@ class Guild(Hashable):
if isinstance(after, datetime.datetime): if isinstance(after, datetime.datetime):
after = Object(id=utils.time_snowflake(after, high=True)) after = Object(id=utils.time_snowflake(after, high=True))
if oldest_first is MISSING: if oldest_first:
reverse = after is not MISSING if after is MISSING:
else: after = OLDEST_OBJECT
reverse = oldest_first
predicate = None predicate = None
if reverse: if oldest_first:
strategy, state = _after_strategy, after strategy, state = _after_strategy, after
if before: if before:
predicate = lambda m: int(m['id']) < before.id predicate = lambda m: int(m['id']) < before.id
else: else:
strategy, state = _before_strategy, before strategy, state = _before_strategy, before
if after and after != OLDEST_OBJECT: if after:
predicate = lambda m: int(m['id']) > after.id predicate = lambda m: int(m['id']) > after.id
# avoid circular import # avoid circular import
@ -3674,8 +3869,6 @@ class Guild(Hashable):
data, raw_entries, state, limit = await strategy(retrieve, state, limit) data, raw_entries, state, limit = await strategy(retrieve, state, limit)
if reverse:
raw_entries = reversed(raw_entries)
if predicate: if predicate:
raw_entries = filter(predicate, raw_entries) raw_entries = filter(predicate, raw_entries)
@ -3748,7 +3941,7 @@ class Guild(Hashable):
) -> None: ) -> None:
"""|coro| """|coro|
Edits the widget of the guild. Edits the widget of the guild. This can also be done with :attr:`~Guild.edit`.
You must have :attr:`~Permissions.manage_guild` to do this. You must have :attr:`~Permissions.manage_guild` to do this.
@ -3776,7 +3969,8 @@ class Guild(Hashable):
if enabled is not MISSING: if enabled is not MISSING:
payload['enabled'] = enabled payload['enabled'] = enabled
await self._state.http.edit_widget(self.id, payload=payload, reason=reason) if payload:
await self._state.http.edit_widget(self.id, payload=payload, reason=reason)
async def chunk(self, *, cache: bool = True) -> List[Member]: async def chunk(self, *, cache: bool = True) -> List[Member]:
"""|coro| """|coro|
@ -3968,7 +4162,7 @@ class Guild(Hashable):
event_type: AutoModRuleEventType, event_type: AutoModRuleEventType,
trigger: AutoModTrigger, trigger: AutoModTrigger,
actions: List[AutoModRuleAction], actions: List[AutoModRuleAction],
enabled: bool = MISSING, enabled: bool = False,
exempt_roles: Sequence[Snowflake] = MISSING, exempt_roles: Sequence[Snowflake] = MISSING,
exempt_channels: Sequence[Snowflake] = MISSING, exempt_channels: Sequence[Snowflake] = MISSING,
reason: str = MISSING, reason: str = MISSING,
@ -3993,7 +4187,7 @@ class Guild(Hashable):
The actions that will be taken when the automod rule is triggered. The actions that will be taken when the automod rule is triggered.
enabled: :class:`bool` enabled: :class:`bool`
Whether the automod rule is enabled. Whether the automod rule is enabled.
Discord will default to ``False``. Defaults to ``False``.
exempt_roles: Sequence[:class:`abc.Snowflake`] exempt_roles: Sequence[:class:`abc.Snowflake`]
A list of roles that will be exempt from the automod rule. A list of roles that will be exempt from the automod rule.
exempt_channels: Sequence[:class:`abc.Snowflake`] exempt_channels: Sequence[:class:`abc.Snowflake`]

13
discord/http.py

@ -1149,6 +1149,7 @@ class HTTPClient:
'available_tags', 'available_tags',
'applied_tags', 'applied_tags',
'default_forum_layout', 'default_forum_layout',
'default_sort_order',
) )
payload = {k: v for k, v in options.items() if k in valid_keys} payload = {k: v for k, v in options.items() if k in valid_keys}
@ -1190,6 +1191,9 @@ class HTTPClient:
'video_quality_mode', 'video_quality_mode',
'default_auto_archive_duration', 'default_auto_archive_duration',
'default_thread_rate_limit_per_user', 'default_thread_rate_limit_per_user',
'default_sort_order',
'default_reaction_emoji',
'default_forum_layout',
'available_tags', 'available_tags',
) )
payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None}) payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None})
@ -1429,6 +1433,12 @@ class HTTPClient:
return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason) return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason)
def edit_guild_mfa_level(
self, guild_id: Snowflake, *, mfa_level: int, reason: Optional[str] = None
) -> Response[guild.GuildMFALevel]:
payload = {'level': mfa_level}
return self.request(Route('POST', '/guilds/{guild_id}/mfa', guild_id=guild_id), json=payload, reason=reason)
def get_template(self, code: str) -> Response[template.Template]: def get_template(self, code: str) -> Response[template.Template]:
return self.request(Route('GET', '/guilds/templates/{code}', code=code)) return self.request(Route('GET', '/guilds/templates/{code}', code=code))
@ -1718,7 +1728,7 @@ class HTTPClient:
params: Dict[str, Any] = {'limit': limit} params: Dict[str, Any] = {'limit': limit}
if before: if before:
params['before'] = before params['before'] = before
if after: if after is not None:
params['after'] = after params['after'] = after
if user_id: if user_id:
params['user_id'] = user_id params['user_id'] = user_id
@ -1904,6 +1914,7 @@ class HTTPClient:
'channel_id', 'channel_id',
'topic', 'topic',
'privacy_level', 'privacy_level',
'send_start_notification',
) )
payload = {k: v for k, v in payload.items() if k in valid_keys} payload = {k: v for k, v in payload.items() if k in valid_keys}

5
discord/interactions.py

@ -195,6 +195,11 @@ class Interaction(Generic[ClientT]):
if self.guild_id: if self.guild_id:
guild = self._state._get_or_create_unavailable_guild(self.guild_id) guild = self._state._get_or_create_unavailable_guild(self.guild_id)
# Upgrade Message.guild in case it's missing with partial guild data
if self.message is not None and self.message.guild is None:
self.message.guild = guild
try: try:
member = data['member'] # type: ignore # The key is optional and handled member = data['member'] # type: ignore # The key is optional and handled
except KeyError: except KeyError:

105
discord/message.py

@ -60,7 +60,7 @@ from .utils import escape_mentions, MISSING
from .http import handle_message_parameters from .http import handle_message_parameters
from .guild import Guild from .guild import Guild
from .mixins import Hashable from .mixins import Hashable
from .sticker import StickerItem from .sticker import StickerItem, GuildSticker
from .threads import Thread from .threads import Thread
from .channel import PartialMessageable from .channel import PartialMessageable
@ -700,6 +700,7 @@ class PartialMessage(Hashable):
- :meth:`TextChannel.get_partial_message` - :meth:`TextChannel.get_partial_message`
- :meth:`VoiceChannel.get_partial_message` - :meth:`VoiceChannel.get_partial_message`
- :meth:`StageChannel.get_partial_message`
- :meth:`Thread.get_partial_message` - :meth:`Thread.get_partial_message`
- :meth:`DMChannel.get_partial_message` - :meth:`DMChannel.get_partial_message`
@ -723,7 +724,7 @@ class PartialMessage(Hashable):
Attributes Attributes
----------- -----------
channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`] channel: Union[:class:`PartialMessageable`, :class:`TextChannel`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`]
The channel associated with this partial message. The channel associated with this partial message.
id: :class:`int` id: :class:`int`
The message ID. The message ID.
@ -737,6 +738,7 @@ class PartialMessage(Hashable):
if not isinstance(channel, PartialMessageable) and channel.type not in ( if not isinstance(channel, PartialMessageable) and channel.type not in (
ChannelType.text, ChannelType.text,
ChannelType.voice, ChannelType.voice,
ChannelType.stage_voice,
ChannelType.news, ChannelType.news,
ChannelType.private, ChannelType.private,
ChannelType.news_thread, ChannelType.news_thread,
@ -744,7 +746,7 @@ class PartialMessage(Hashable):
ChannelType.private_thread, ChannelType.private_thread,
): ):
raise TypeError( raise TypeError(
f'expected PartialMessageable, TextChannel, VoiceChannel, DMChannel or Thread not {type(channel)!r}' f'expected PartialMessageable, TextChannel, StageChannel, VoiceChannel, DMChannel or Thread not {type(channel)!r}'
) )
self.channel: MessageableChannel = channel self.channel: MessageableChannel = channel
@ -1252,6 +1254,86 @@ class PartialMessage(Hashable):
) )
return Thread(guild=self.guild, state=self._state, data=data) return Thread(guild=self.guild, state=self._state, data=data)
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
file: File = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message:
...
@overload
async def reply(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embeds: Sequence[Embed] = ...,
files: Sequence[File] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ...,
view: View = ...,
suppress_embeds: bool = ...,
silent: bool = ...,
) -> Message:
...
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
"""|coro| """|coro|
@ -1357,7 +1439,7 @@ class Message(PartialMessage, Hashable):
A list of embeds the message has. A list of embeds the message has.
If :attr:`Intents.message_content` is not enabled this will always be an empty list If :attr:`Intents.message_content` is not enabled this will always be an empty list
unless the bot is mentioned or the message is a direct message. unless the bot is mentioned or the message is a direct message.
channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`] channel: Union[:class:`TextChannel`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`, :class:`PartialMessageable`]
The :class:`TextChannel` or :class:`Thread` that the message was sent from. 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. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
reference: Optional[:class:`~discord.MessageReference`] reference: Optional[:class:`~discord.MessageReference`]
@ -2015,6 +2097,21 @@ class Message(PartialMessage, Hashable):
months = '1 month' if total_months == 1 else f'{total_months} months' months = '1 month' if total_months == 1 else f'{total_months} months'
return f'{self.author.name} joined {self.role_subscription.tier_name} and has been a subscriber of {self.guild} for {months}!' return f'{self.author.name} joined {self.role_subscription.tier_name} and has been a subscriber of {self.guild} for {months}!'
if self.type is MessageType.stage_start:
return f'{self.author.name} started **{self.content}**.'
if self.type is MessageType.stage_end:
return f'{self.author.name} ended **{self.content}**.'
if self.type is MessageType.stage_speaker:
return f'{self.author.name} is now a speaker.'
if self.type is MessageType.stage_raise_hand:
return f'{self.author.name} requested to speak.'
if self.type is MessageType.stage_topic:
return f'{self.author.name} changed Stage topic: **{self.content}**.'
# Fallback for unknown message types # Fallback for unknown message types
return '' return ''

46
discord/permissions.py

@ -177,7 +177,7 @@ class Permissions(BaseFlags):
"""A factory method that creates a :class:`Permissions` with all """A factory method that creates a :class:`Permissions` with all
permissions set to ``True``. permissions set to ``True``.
""" """
return cls(0b11111111111111111111111111111111111111111) return cls(0b1111111111111111111111111111111111111111111111)
@classmethod @classmethod
def _timeout_mask(cls) -> int: def _timeout_mask(cls) -> int:
@ -204,7 +204,7 @@ class Permissions(BaseFlags):
``True`` and the guild-specific ones set to ``False``. The guild-specific ``True`` and the guild-specific ones set to ``False``. The guild-specific
permissions are currently: permissions are currently:
- :attr:`manage_emojis` - :attr:`manage_guild_expressions`
- :attr:`view_audit_log` - :attr:`view_audit_log`
- :attr:`view_guild_insights` - :attr:`view_guild_insights`
- :attr:`manage_guild` - :attr:`manage_guild`
@ -221,8 +221,11 @@ class Permissions(BaseFlags):
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`, Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
:attr:`use_external_stickers`, :attr:`send_messages_in_threads` and :attr:`use_external_stickers`, :attr:`send_messages_in_threads` and
:attr:`request_to_speak` permissions. :attr:`request_to_speak` permissions.
.. versionchanged:: 2.3
Added :attr:`use_soundboard`
""" """
return cls(0b111110110110011111101111111111101010001) return cls(0b1000111110110110011111101111111111101010001)
@classmethod @classmethod
def general(cls) -> Self: def general(cls) -> Self:
@ -265,7 +268,7 @@ class Permissions(BaseFlags):
def voice(cls) -> Self: def voice(cls) -> Self:
"""A factory method that creates a :class:`Permissions` with all """A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to ``True``.""" "Voice" permissions from the official Discord UI set to ``True``."""
return cls(0b1000000000000011111100000000001100000000) return cls(0b1001001000000000000011111100000000001100000000)
@classmethod @classmethod
def stage(cls) -> Self: def stage(cls) -> Self:
@ -305,7 +308,7 @@ class Permissions(BaseFlags):
- :attr:`manage_messages` - :attr:`manage_messages`
- :attr:`manage_roles` - :attr:`manage_roles`
- :attr:`manage_webhooks` - :attr:`manage_webhooks`
- :attr:`manage_emojis_and_stickers` - :attr:`manage_guild_expressions`
- :attr:`manage_threads` - :attr:`manage_threads`
- :attr:`moderate_members` - :attr:`moderate_members`
@ -544,13 +547,21 @@ class Permissions(BaseFlags):
return 1 << 29 return 1 << 29
@flag_value @flag_value
def manage_guild_expressions(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis, stickers, and soundboard sounds.
.. versionadded:: 2.3
"""
return 1 << 30
@make_permission_alias('manage_guild_expressions')
def manage_emojis(self) -> int: def manage_emojis(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis.""" """:class:`bool`: An alias for :attr:`manage_guild_expressions`."""
return 1 << 30 return 1 << 30
@make_permission_alias('manage_emojis') @make_permission_alias('manage_guild_expressions')
def manage_emojis_and_stickers(self) -> int: def manage_emojis_and_stickers(self) -> int:
""":class:`bool`: An alias for :attr:`manage_emojis`. """:class:`bool`: An alias for :attr:`manage_guild_expressions`.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
@ -644,6 +655,22 @@ class Permissions(BaseFlags):
""" """
return 1 << 40 return 1 << 40
@flag_value
def use_soundboard(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use the soundboard.
.. versionadded:: 2.3
"""
return 1 << 42
@flag_value
def use_external_sounds(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use sounds from other guilds.
.. versionadded:: 2.3
"""
return 1 << 45
def _augment_from_permissions(cls): def _augment_from_permissions(cls):
cls.VALID_NAMES = set(Permissions.VALID_FLAGS) cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
@ -745,6 +772,7 @@ class PermissionOverwrite:
manage_roles: Optional[bool] manage_roles: Optional[bool]
manage_permissions: Optional[bool] manage_permissions: Optional[bool]
manage_webhooks: Optional[bool] manage_webhooks: Optional[bool]
manage_guild_expressions: Optional[bool]
manage_emojis: Optional[bool] manage_emojis: Optional[bool]
manage_emojis_and_stickers: Optional[bool] manage_emojis_and_stickers: Optional[bool]
use_application_commands: Optional[bool] use_application_commands: Optional[bool]
@ -758,6 +786,8 @@ class PermissionOverwrite:
use_external_stickers: Optional[bool] use_external_stickers: Optional[bool]
use_embedded_activities: Optional[bool] use_embedded_activities: Optional[bool]
moderate_members: Optional[bool] moderate_members: Optional[bool]
use_soundboard: Optional[bool]
use_external_sounds: Optional[bool]
def __init__(self, **kwargs: Optional[bool]): def __init__(self, **kwargs: Optional[bool]):
self._values: Dict[str, Optional[bool]] = {} self._values: Dict[str, Optional[bool]] = {}

120
discord/scheduled_event.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, AsyncIterator, Dict, Optional, Union from typing import TYPE_CHECKING, AsyncIterator, Dict, Optional, Union, overload, Literal
from .asset import Asset from .asset import Asset
from .enums import EventStatus, EntityType, PrivacyLevel, try_enum from .enums import EventStatus, EntityType, PrivacyLevel, try_enum
@ -298,6 +298,87 @@ class ScheduledEvent(Hashable):
return await self.__modify_status(EventStatus.cancelled, reason) return await self.__modify_status(EventStatus.cancelled, reason)
@overload
async def edit(
self,
*,
name: str = ...,
description: str = ...,
start_time: datetime = ...,
end_time: Optional[datetime] = ...,
privacy_level: PrivacyLevel = ...,
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def edit(
self,
*,
name: str = ...,
description: str = ...,
channel: Snowflake,
start_time: datetime = ...,
end_time: Optional[datetime] = ...,
privacy_level: PrivacyLevel = ...,
entity_type: Literal[EntityType.voice, EntityType.stage_instance],
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def edit(
self,
*,
name: str = ...,
description: str = ...,
start_time: datetime = ...,
end_time: datetime = ...,
privacy_level: PrivacyLevel = ...,
entity_type: Literal[EntityType.external],
status: EventStatus = ...,
image: bytes = ...,
location: str,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def edit(
self,
*,
name: str = ...,
description: str = ...,
channel: Union[VoiceChannel, StageChannel],
start_time: datetime = ...,
end_time: Optional[datetime] = ...,
privacy_level: PrivacyLevel = ...,
status: EventStatus = ...,
image: bytes = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@overload
async def edit(
self,
*,
name: str = ...,
description: str = ...,
start_time: datetime = ...,
end_time: datetime = ...,
privacy_level: PrivacyLevel = ...,
status: EventStatus = ...,
image: bytes = ...,
location: str,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
async def edit( async def edit(
self, self,
*, *,
@ -414,24 +495,34 @@ class ScheduledEvent(Hashable):
payload['image'] = image_as_str payload['image'] = image_as_str
entity_type = entity_type or getattr(channel, '_scheduled_event_entity_type', MISSING) entity_type = entity_type or getattr(channel, '_scheduled_event_entity_type', MISSING)
if entity_type is None: if entity_type is MISSING:
raise TypeError( if channel and isinstance(channel, Object):
f'invalid GuildChannel type passed, must be VoiceChannel or StageChannel not {channel.__class__.__name__}' if channel.type is VoiceChannel:
) entity_type = EntityType.voice
elif channel.type is StageChannel:
if entity_type is not MISSING: entity_type = EntityType.stage_instance
elif location not in (MISSING, None):
entity_type = EntityType.external
else:
if not isinstance(entity_type, EntityType): if not isinstance(entity_type, EntityType):
raise TypeError('entity_type must be of type EntityType') raise TypeError('entity_type must be of type EntityType')
payload['entity_type'] = entity_type.value payload['entity_type'] = entity_type.value
if entity_type is None:
raise TypeError(
f'invalid GuildChannel type passed, must be VoiceChannel or StageChannel not {channel.__class__.__name__}'
)
_entity_type = entity_type or self.entity_type _entity_type = entity_type or self.entity_type
_entity_type_changed = _entity_type is not self.entity_type
if _entity_type in (EntityType.stage_instance, EntityType.voice): if _entity_type in (EntityType.stage_instance, EntityType.voice):
if channel is MISSING or channel is None: if channel is MISSING or channel is None:
raise TypeError('channel must be set when entity_type is voice or stage_instance') if _entity_type_changed:
raise TypeError('channel must be set when entity_type is voice or stage_instance')
payload['channel_id'] = channel.id else:
payload['channel_id'] = channel.id
if location not in (MISSING, None): if location not in (MISSING, None):
raise TypeError('location cannot be set when entity_type is voice or stage_instance') raise TypeError('location cannot be set when entity_type is voice or stage_instance')
@ -442,11 +533,12 @@ class ScheduledEvent(Hashable):
payload['channel_id'] = None payload['channel_id'] = None
if location is MISSING or location is None: if location is MISSING or location is None:
raise TypeError('location must be set when entity_type is external') if _entity_type_changed:
raise TypeError('location must be set when entity_type is external')
metadata['location'] = location else:
metadata['location'] = location
if end_time is MISSING or end_time is None: if not self.end_time and (end_time is MISSING or end_time is None):
raise TypeError('end_time must be set when entity_type is external') raise TypeError('end_time must be set when entity_type is external')
if end_time is not MISSING: if end_time is not MISSING:

2
discord/sticker.py

@ -414,7 +414,7 @@ class GuildSticker(Sticker):
def _from_data(self, data: GuildStickerPayload) -> None: def _from_data(self, data: GuildStickerPayload) -> None:
super()._from_data(data) super()._from_data(data)
self.available: bool = data['available'] self.available: bool = data.get('available', True)
self.guild_id: int = int(data['guild_id']) self.guild_id: int = int(data['guild_id'])
user = data.get('user') user = data.get('user')
self.user: Optional[User] = self._state.store_user(user) if user else None self.user: Optional[User] = self._state.store_user(user) if user else None

17
discord/types/appinfo.py

@ -44,10 +44,14 @@ class BaseAppInfo(TypedDict):
icon: Optional[str] icon: Optional[str]
summary: str summary: str
description: str description: str
flags: int
cover_image: NotRequired[str]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]
rpc_origins: NotRequired[List[str]]
class AppInfo(BaseAppInfo): class AppInfo(BaseAppInfo):
rpc_origins: List[str]
owner: User owner: User
bot_public: bool bot_public: bool
bot_require_code_grant: bool bot_require_code_grant: bool
@ -55,8 +59,6 @@ class AppInfo(BaseAppInfo):
guild_id: NotRequired[Snowflake] guild_id: NotRequired[Snowflake]
primary_sku_id: NotRequired[Snowflake] primary_sku_id: NotRequired[Snowflake]
slug: NotRequired[str] slug: NotRequired[str]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]
hook: NotRequired[bool] hook: NotRequired[bool]
max_participants: NotRequired[int] max_participants: NotRequired[int]
tags: NotRequired[List[str]] tags: NotRequired[List[str]]
@ -66,13 +68,12 @@ class AppInfo(BaseAppInfo):
class PartialAppInfo(BaseAppInfo, total=False): class PartialAppInfo(BaseAppInfo, total=False):
rpc_origins: List[str]
cover_image: str
hook: bool hook: bool
terms_of_service_url: str
privacy_policy_url: str
max_participants: int max_participants: int
flags: int approximate_guild_count: int
redirect_uris: List[str]
interactions_endpoint_url: Optional[str]
role_connections_verification_url: Optional[str]
class GatewayAppInfo(TypedDict): class GatewayAppInfo(TypedDict):

6
discord/types/automod.py

@ -45,9 +45,13 @@ class _AutoModerationActionMetadataTimeout(TypedDict):
duration_seconds: int duration_seconds: int
class _AutoModerationActionMetadataCustomMessage(TypedDict):
custom_message: str
class _AutoModerationActionBlockMessage(TypedDict): class _AutoModerationActionBlockMessage(TypedDict):
type: Literal[1] type: Literal[1]
metadata: NotRequired[Empty] metadata: NotRequired[_AutoModerationActionMetadataCustomMessage]
class _AutoModerationActionAlert(TypedDict): class _AutoModerationActionAlert(TypedDict):

2
discord/types/channel.py

@ -134,6 +134,7 @@ class ForumTag(TypedDict):
emoji_name: Optional[str] emoji_name: Optional[str]
ForumOrderType = Literal[0, 1]
ForumLayoutType = Literal[0, 1, 2] ForumLayoutType = Literal[0, 1, 2]
@ -141,6 +142,7 @@ class ForumChannel(_BaseTextChannel):
type: Literal[15] type: Literal[15]
available_tags: List[ForumTag] available_tags: List[ForumTag]
default_reaction_emoji: Optional[DefaultReaction] default_reaction_emoji: Optional[DefaultReaction]
default_sort_order: Optional[ForumOrderType]
default_forum_layout: NotRequired[ForumLayoutType] default_forum_layout: NotRequired[ForumLayoutType]
flags: NotRequired[int] flags: NotRequired[int]

4
discord/types/guild.py

@ -161,6 +161,10 @@ class GuildPrune(TypedDict):
pruned: Optional[int] pruned: Optional[int]
class GuildMFALevel(TypedDict):
level: MFALevel
class ChannelPositionUpdate(TypedDict): class ChannelPositionUpdate(TypedDict):
id: Snowflake id: Snowflake
position: Optional[int] position: Optional[int]

2
discord/types/sticker.py

@ -55,7 +55,7 @@ class StandardSticker(BaseSticker):
class GuildSticker(BaseSticker): class GuildSticker(BaseSticker):
type: Literal[2] type: Literal[2]
available: bool available: NotRequired[bool]
guild_id: Snowflake guild_id: Snowflake
user: NotRequired[User] user: NotRequired[User]

2
discord/ui/item.py

@ -40,7 +40,7 @@ if TYPE_CHECKING:
from .view import View from .view import View
from ..components import Component from ..components import Component
I = TypeVar('I', bound='Item') I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='View', covariant=True)
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]

14
discord/ui/select.py

@ -22,7 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Callable, Union, Dict, overload from typing import Any, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Callable, Union, Dict, overload
from contextvars import ContextVar from contextvars import ContextVar
import inspect import inspect
import os import os
@ -71,12 +71,12 @@ if TYPE_CHECKING:
] ]
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='View', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect') BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
SelectT = TypeVar('SelectT', bound='Select') SelectT = TypeVar('SelectT', bound='Select[Any]')
UserSelectT = TypeVar('UserSelectT', bound='UserSelect') UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]')
RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect') RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]')
ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect') ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]')
MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect') MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]')
SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT]
selected_values: ContextVar[Dict[str, List[PossibleValue]]] = ContextVar('selected_values') selected_values: ContextVar[Dict[str, List[PossibleValue]]] = ContextVar('selected_values')

12
discord/webhook/sync.py

@ -636,9 +636,9 @@ class SyncWebhook(BaseWebhook):
Returns Returns
-------- --------
:class:`Webhook` :class:`SyncWebhook`
A partial :class:`Webhook`. A partial :class:`SyncWebhook`.
A partial webhook is just a webhook object with an ID and a token. A partial :class:`SyncWebhook` is just a :class:`SyncWebhook` object with an ID and a token.
""" """
data: WebhookPayload = { data: WebhookPayload = {
'id': id, 'id': id,
@ -678,9 +678,9 @@ class SyncWebhook(BaseWebhook):
Returns Returns
-------- --------
:class:`Webhook` :class:`SyncWebhook`
A partial :class:`Webhook`. A partial :class:`SyncWebhook`.
A partial webhook is just a webhook object with an ID and a token. A partial :class:`SyncWebhook` is just a :class:`SyncWebhook` object with an ID and a token.
""" """
m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url) m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
if m is None: if m is None:

2
discord/widget.py

@ -226,7 +226,7 @@ class Widget:
The guild's name. The guild's name.
channels: List[:class:`WidgetChannel`] channels: List[:class:`WidgetChannel`]
The accessible voice channels in the guild. The accessible voice channels in the guild.
members: List[:class:`Member`] members: List[:class:`WidgetMember`]
The online members in the guild. Offline members The online members in the guild. Offline members
do not appear in the widget. do not appear in the widget.

86
docs/api.rst

@ -1653,6 +1653,31 @@ of :class:`enum.Enum`.
The system message sent when a user is given an advertisement to purchase a premium tier for The system message sent when a user is given an advertisement to purchase a premium tier for
an application during an interaction. an application during an interaction.
.. versionadded:: 2.2
.. attribute:: stage_start
The system message sent when the stage starts.
.. versionadded:: 2.2
.. attribute:: stage_end
The system message sent when the stage ends.
.. versionadded:: 2.2
.. attribute:: stage_speaker
The system message sent when the stage speaker changes.
.. versionadded:: 2.2
.. attribute:: stage_raise_hand
The system message sent when a user is requesting to speak by raising their hands.
.. versionadded:: 2.2
.. attribute:: stage_topic
The system message sent when the stage topic changes.
.. versionadded:: 2.2 .. versionadded:: 2.2
.. attribute:: guild_application_premium_subscription .. attribute:: guild_application_premium_subscription
@ -1950,6 +1975,8 @@ of :class:`enum.Enum`.
- :attr:`~AuditLogDiff.verification_level` - :attr:`~AuditLogDiff.verification_level`
- :attr:`~AuditLogDiff.widget_channel` - :attr:`~AuditLogDiff.widget_channel`
- :attr:`~AuditLogDiff.widget_enabled` - :attr:`~AuditLogDiff.widget_enabled`
- :attr:`~AuditLogDiff.premium_progress_bar_enabled`
- :attr:`~AuditLogDiff.system_channel_flags`
.. attribute:: channel_create .. attribute:: channel_create
@ -1991,6 +2018,9 @@ of :class:`enum.Enum`.
- :attr:`~AuditLogDiff.rtc_region` - :attr:`~AuditLogDiff.rtc_region`
- :attr:`~AuditLogDiff.video_quality_mode` - :attr:`~AuditLogDiff.video_quality_mode`
- :attr:`~AuditLogDiff.default_auto_archive_duration` - :attr:`~AuditLogDiff.default_auto_archive_duration`
- :attr:`~AuditLogDiff.nsfw`
- :attr:`~AuditLogDiff.slowmode_delay`
- :attr:`~AuditLogDiff.user_limit`
.. attribute:: channel_delete .. attribute:: channel_delete
@ -2007,6 +2037,9 @@ of :class:`enum.Enum`.
- :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.name`
- :attr:`~AuditLogDiff.type` - :attr:`~AuditLogDiff.type`
- :attr:`~AuditLogDiff.overwrites` - :attr:`~AuditLogDiff.overwrites`
- :attr:`~AuditLogDiff.flags`
- :attr:`~AuditLogDiff.nsfw`
- :attr:`~AuditLogDiff.slowmode_delay`
.. attribute:: overwrite_create .. attribute:: overwrite_create
@ -2078,7 +2111,7 @@ of :class:`enum.Enum`.
When this is the action, the type of :attr:`~AuditLogEntry.extra` is When this is the action, the type of :attr:`~AuditLogEntry.extra` is
set to an unspecified proxy object with two attributes: set to an unspecified proxy object with two attributes:
- ``delete_members_days``: An integer specifying how far the prune was. - ``delete_member_days``: An integer specifying how far the prune was.
- ``members_removed``: An integer specifying how many members were removed. - ``members_removed``: An integer specifying how many members were removed.
When this is the action, :attr:`~AuditLogEntry.changes` is empty. When this is the action, :attr:`~AuditLogEntry.changes` is empty.
@ -3291,6 +3324,21 @@ of :class:`enum.Enum`.
Prompt options are displayed as a drop-down. Prompt options are displayed as a drop-down.
.. class:: ForumOrderType
Represents how a forum's posts are sorted in the client.
.. versionadded:: 2.3
.. attribute:: latest_activity
Sort forum posts by activity.
.. attribute:: creation_date
Sort forum posts by creation time (from most recent to oldest).
.. _discord-api-audit-logs: .. _discord-api-audit-logs:
Audit Log Data Audit Log Data
@ -3926,6 +3974,42 @@ AuditLogDiff
:type: List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`] :type: List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`]
.. attribute:: premium_progress_bar_enabled
The guild’s display setting to show boost progress bar.
:type: :class:`bool`
.. attribute:: system_channel_flags
The guild’s system channel settings.
See also :attr:`Guild.system_channel_flags`
:type: :class:`SystemChannelFlags`
.. attribute:: nsfw
Whether the channel is marked as “not safe for work” or “age restricted”.
:type: :class:`bool`
.. attribute:: user_limit
The channel’s limit for number of members that can be in a voice or stage channel.
See also :attr:`VoiceChannel.user_limit` and :attr:`StageChannel.user_limit`
:type: :class:`int`
.. attribute:: flags
The channel flags associated with this thread or forum post.
See also :attr:`ForumChannel.flags` and :attr:`Thread.flags`
:type: :class:`ChannelFlags`
.. this is currently missing the following keys: reason and application_id .. this is currently missing the following keys: reason and application_id
I'm not sure how to port these I'm not sure how to port these

2
docs/ext/commands/api.rst

@ -256,7 +256,7 @@ GroupCog
.. attributetable:: discord.ext.commands.GroupCog .. attributetable:: discord.ext.commands.GroupCog
.. autoclass:: discord.ext.commands.GroupCog .. autoclass:: discord.ext.commands.GroupCog
:members: :members: interaction_check
CogMeta CogMeta

1
docs/ext/commands/commands.rst

@ -1147,6 +1147,7 @@ If you want a more robust error system, you can derive from the exception and ra
return True return True
return commands.check(predicate) return commands.check(predicate)
@bot.command()
@guild_only() @guild_only()
async def test(ctx): async def test(ctx):
await ctx.send('Hey this is not a DM! Nice.') await ctx.send('Hey this is not a DM! Nice.')

90
docs/whats_new.rst

@ -11,6 +11,96 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed This page keeps a detailed human friendly rendering of what's new and changed
in specific versions. in specific versions.
.. _vp2p2p2:
v2.2.2
-------
Bug Fixes
~~~~~~~~~~
- Fix UDP discovery in voice not using new 74 byte layout which caused voice to break (:issue:`9277`, :issue:`9278`)
.. _vp2p2p0:
v2.2.0
-------
New Features
~~~~~~~~~~~~
- Add support for new :func:`on_audit_log_entry_create` event
- Add support for silent messages via ``silent`` parameter in :meth:`abc.Messageable.send`
- This is queryable via :attr:`MessageFlags.suppress_notifications`
- Implement :class:`abc.Messageable` for :class:`StageChannel` (:issue:`9248`)
- Add setter for :attr:`discord.ui.ChannelSelect.channel_types` (:issue:`9068`)
- Add support for custom messages in automod via :attr:`AutoModRuleAction.custom_message` (:issue:`9267`)
- Add :meth:`ForumChannel.get_thread` (:issue:`9106`)
- Add :attr:`StageChannel.slowmode_delay` and :attr:`VoiceChannel.slowmode_delay` (:issue:`9111`)
- Add support for editing the slowmode for :class:`StageChannel` and :class:`VoiceChannel` (:issue:`9111`)
- Add :attr:`Locale.indonesian`
- Add ``delete_after`` keyword argument to :meth:`Interaction.edit_message` (:issue:`9415`)
- Add ``delete_after`` keyword argument to :meth:`InteractionMessage.edit` (:issue:`9206`)
- Add support for member flags (:issue:`9204`)
- Accessible via :attr:`Member.flags` and has a type of :class:`MemberFlags`
- Support ``bypass_verification`` within :meth:`Member.edit`
- Add support for passing a client to :meth:`Webhook.from_url` and :meth:`Webhook.partial`
- This allows them to use views (assuming they are "bot owned" webhooks)
- Add :meth:`Colour.dark_embed` and :meth:`Colour.light_embed` (:issue:`9219`)
- Add support for many more parameters within :meth:`Guild.create_stage_channel` (:issue:`9245`)
- Add :attr:`AppInfo.role_connections_verification_url`
- Add support for :attr:`ForumChannel.default_layout`
- Add various new :class:`MessageType` values such as ones related to stage channel and role subscriptions
- Add support for role subscription related attributes
- :class:`RoleSubscriptionInfo` within :attr:`Message.role_subscription`
- :attr:`MessageType.role_subscription_purchase`
- :attr:`SystemChannelFlags.role_subscription_purchase_notifications`
- :attr:`SystemChannelFlags.role_subscription_purchase_notification_replies`
- :attr:`RoleTags.subscription_listing_id`
- :meth:`RoleTags.is_available_for_purchase`
- Add support for checking if a role is a linked role under :meth:`RoleTags.is_guild_connection`
- Add support for GIF sticker type
- Add support for :attr:`Message.application_id` and :attr:`Message.position`
- Add :func:`utils.maybe_coroutine` helper
- Add :attr:`ScheduledEvent.creator_id` attribute
- |commands| Add support for :meth:`~ext.commands.Cog.interaction_check` for :class:`~ext.commands.GroupCog` (:issue:`9189`)
Bug Fixes
~~~~~~~~~~
- Fix views not being removed from message store backing leading to a memory leak when used from an application command context
- Fix async iterators requesting past their bounds when using ``oldest_first`` and ``after`` or ``before`` (:issue:`9093`)
- Fix :meth:`Guild.audit_logs` pagination logic being buggy when using ``after`` (:issue:`9269`)
- Fix :attr:`Message.channel` sometimes being :class:`Object` instead of :class:`PartialMessageable`
- Fix :class:`ui.View` not properly calling ``super().__init_subclass__`` (:issue:`9231`)
- Fix ``available_tags`` and ``default_thread_slowmode_delay`` not being respected in :meth:`Guild.create_forum`
- Fix :class:`AutoModTrigger` ignoring ``allow_list`` with type keyword (:issue:`9107`)
- Fix implicit permission resolution for :class:`Thread` (:issue:`9153`)
- Fix :meth:`AutoModRule.edit` to work with actual snowflake types such as :class:`Object` (:issue:`9159`)
- Fix :meth:`Webhook.send` returning :class:`ForumChannel` for :attr:`WebhookMessage.channel`
- When a lookup for :attr:`AuditLogEntry.target` fails, it will fallback to :class:`Object` with the appropriate :attr:`Object.type` (:issue:`9171`)
- Fix :attr:`AuditLogDiff.type` for integrations returning :class:`ChannelType` instead of :class:`str` (:issue:`9200`)
- Fix :attr:`AuditLogDiff.type` for webhooks returning :class:`ChannelType` instead of :class:`WebhookType` (:issue:`9251`)
- Fix webhooks and interactions not properly closing files after the request has completed
- Fix :exc:`NameError` in audit log target for app commands
- Fix :meth:`ScheduledEvent.edit` requiring some arguments to be passed in when unnecessary (:issue:`9261`, :issue:`9268`)
- |commands| Explicit set a traceback for hybrid command invocations (:issue:`9205`)
Miscellaneous
~~~~~~~~~~~~~~
- Add colour preview for the colours predefined in :class:`Colour`
- Finished views are no longer stored by the library when sending them (:issue:`9235`)
- Force enable colour logging for the default logging handler when run under Docker.
- Add various overloads for :meth:`Client.wait_for` to aid in static analysis (:issue:`9184`)
- :class:`Interaction` can now optionally take a generic parameter, ``ClientT`` to represent the type for :attr:`Interaction.client`
- |commands| Respect :attr:`~ext.commands.Command.ignore_extra` for :class:`~discord.ext.commands.FlagConverter` keyword-only parameters
- |commands| Change :attr:`Paginator.pages <ext.commands.Paginator.pages>` to not prematurely close (:issue:`9257`)
.. _vp2p1p1: .. _vp2p1p1:
v2.1.1 v2.1.1

Loading…
Cancel
Save