diff --git a/discord/__init__.py b/discord/__init__.py index 51de8d09c..81d7bf570 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.2.0a' +__version__ = '2.3.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -79,7 +79,7 @@ class VersionInfo(NamedTuple): 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()) diff --git a/discord/abc.py b/discord/abc.py index 6e3125f40..c6b63920f 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -83,7 +83,15 @@ if TYPE_CHECKING: from .channel import CategoryChannel from .embeds import Embed 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 .enums import InviteTarget from .ui.view import View @@ -97,7 +105,7 @@ if TYPE_CHECKING: SnowflakeList, ) - PartialMessageableChannel = Union[TextChannel, VoiceChannel, Thread, DMChannel, PartialMessageable] + PartialMessageableChannel = Union[TextChannel, VoiceChannel, StageChannel, Thread, DMChannel, PartialMessageable] MessageableChannel = Union[PartialMessageableChannel, GroupChannel] SnowflakeTime = Union["Snowflake", datetime] @@ -118,7 +126,7 @@ async def _single_delete_strategy(messages: Iterable[Message], *, reason: Option async def _purge_helper( - channel: Union[Thread, TextChannel, VoiceChannel], + channel: Union[Thread, TextChannel, VocalGuildChannel], *, limit: Optional[int] = 100, check: Callable[[Message], bool] = MISSING, @@ -1284,6 +1292,7 @@ class Messageable: - :class:`~discord.TextChannel` - :class:`~discord.VoiceChannel` + - :class:`~discord.StageChannel` - :class:`~discord.DMChannel` - :class:`~discord.GroupChannel` - :class:`~discord.PartialMessageable` diff --git a/discord/appinfo.py b/discord/appinfo.py index 18c97228b..129e543cb 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -166,7 +166,7 @@ class AppInfo: self.name: str = data['name'] self.description: str = data['description'] 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_require_code_grant: bool = data['bot_require_code_grant'] self.owner: User = state.create_user(data['owner']) @@ -255,6 +255,24 @@ class PartialAppInfo: The application's terms of service URL, if set. privacy_policy_url: Optional[:class:`str`] 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__ = ( @@ -268,6 +286,11 @@ class PartialAppInfo: 'privacy_policy_url', '_icon', '_flags', + '_cover_image', + 'approximate_guild_count', + 'redirect_uris', + 'interactions_endpoint_url', + 'role_connections_verification_url', ) def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload): @@ -276,11 +299,16 @@ class PartialAppInfo: self.name: str = data['name'] self._icon: Optional[str] = data.get('icon') self._flags: int = data.get('flags', 0) + self._cover_image: Optional[str] = data.get('cover_image') self.description: str = data['description'] self.rpc_origins: Optional[List[str]] = data.get('rpc_origins') self.verify_key: str = data['verify_key'] 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.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: 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 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 def flags(self) -> ApplicationFlags: """:class:`ApplicationFlags`: The application's flags. diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 72cf8f854..47f397a8a 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -521,7 +521,7 @@ class _AuditLogProxyMessageBulkDelete(_AuditLogProxy): class _AuditLogProxyAutoModAction(_AuditLogProxy): automod_rule_name: str automod_rule_trigger_type: str - channel: Union[abc.GuildChannel, Thread] + channel: Optional[Union[abc.GuildChannel, Thread]] 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_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( automod_rule_name=extra['auto_moderation_rule_name'], automod_rule_trigger_type=enums.try_enum( 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_'): @@ -723,11 +727,12 @@ class AuditLogEntry(Hashable): if self.action.target_type is None: return None + if self._target_id is None: + return None + try: converter = getattr(self, '_convert_target_' + self.action.target_type) except AttributeError: - if self._target_id is None: - return None return Object(id=self._target_id) else: return converter(self._target_id) diff --git a/discord/automod.py b/discord/automod.py index c90e80389..84a00c87e 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -25,8 +25,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations 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 .flags import AutoModPresets @@ -59,6 +58,9 @@ __all__ = ( class AutoModRuleAction: """Represents an auto moderation's rule action. + .. note:: + Only one of ``channel_id``, ``duration``, or ``custom_message`` can be used. + .. versionadded:: 2.0 Attributes @@ -73,40 +75,65 @@ class AutoModRuleAction: The duration of the timeout to apply, if any. Has a maximum of 28 days. 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.duration: Optional[datetime.timedelta] = duration - if channel_id and duration: - raise ValueError('Please provide only one of ``channel`` or ``duration``') + self.custom_message: Optional[str] = custom_message + + 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: self.type = AutoModRuleActionType.send_alert_message elif duration: self.type = AutoModRuleActionType.timeout - else: - self.type = AutoModRuleActionType.block_message def __repr__(self) -> str: return f'' @classmethod def from_data(cls, data: AutoModerationActionPayload) -> Self: - type_ = try_enum(AutoModRuleActionType, data['type']) if data['type'] == AutoModRuleActionType.timeout.value: duration_seconds = data['metadata']['duration_seconds'] return cls(duration=datetime.timedelta(seconds=duration_seconds)) elif data['type'] == AutoModRuleActionType.send_alert_message.value: channel_id = int(data['metadata']['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]: 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 elif self.type is AutoModRuleActionType.send_alert_message: ret['metadata'] = {'channel_id': str(self.channel_id)} @@ -139,13 +166,13 @@ class AutoModTrigger: The type of trigger. keyword_filter: List[:class:`str`] 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`. regex_patterns: List[:class:`str`] The regex pattern that will trigger the filter. The syntax is based off of `Rust's regex syntax `_. - Maximum of 10. Regex strings can only be up to 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` @@ -153,7 +180,8 @@ class AutoModTrigger: presets: :class:`AutoModPresets` The presets used with the preset keyword filter. 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` The total number of user and role mentions a message can contain. Has a maximum of 50. diff --git a/discord/channel.py b/discord/channel.py index 8aecf7838..3c93832f3 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -47,7 +47,7 @@ import datetime import discord.abc from .scheduled_event import ScheduledEvent 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 . import utils 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. .. 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__ = ( @@ -176,6 +180,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): '_type', 'last_message_id', 'default_auto_archive_duration', + 'default_thread_slowmode_delay', ) 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. 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_thread_slowmode_delay: int = data.get('default_thread_rate_limit_per_user', 0) 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._fill_overwrites(data) @@ -301,6 +307,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): category: Optional[CategoryChannel] = ..., slowmode_delay: int = ..., default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., type: ChannelType = ..., overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., ) -> TextChannel: @@ -359,7 +366,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): Must be one of ``60``, ``1440``, ``4320``, or ``10080``. .. 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 ------ ValueError @@ -878,7 +888,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): 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__ = ( 'name', 'id', @@ -901,6 +911,9 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha self.id: int = int(data['id']) self._update(guild, data) + async def _get_channel(self) -> Self: + return self + def _get_voice_client_key(self) -> Tuple[int, str]: return self.guild.id, 'guild_id' @@ -988,103 +1001,6 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha base.value &= ~denied.value 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 def last_message(self) -> Optional[Message]: """Retrieves the last message from this channel in cache. @@ -1129,7 +1045,7 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): 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: """|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) 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) 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) @@ -1358,6 +1368,7 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., rtc_region: Optional[str] = ..., video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., reason: Optional[str] = ..., ) -> VoiceChannel: ... @@ -1492,6 +1503,11 @@ class StageChannel(VocalGuildChannel): The camera video quality for the stage 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.2 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. @@ -1578,7 +1594,12 @@ class StageChannel(VocalGuildChannel): return utils.get(self.guild.stage_instances, channel_id=self.id) 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: """|coro| @@ -1594,6 +1615,11 @@ class StageChannel(VocalGuildChannel): The stage instance's topic. privacy_level: :class:`PrivacyLevel` 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` 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['send_start_notification'] = send_start_notification + data = await self._state.http.create_stage_instance(**payload, reason=reason) return StageInstance(guild=self.guild, state=self._state, data=data) @@ -1665,6 +1693,7 @@ class StageChannel(VocalGuildChannel): overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ..., rtc_region: Optional[str] = ..., video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., reason: Optional[str] = ..., ) -> StageChannel: ... @@ -2147,6 +2176,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): Defaults to :attr:`ForumLayoutType.not_set`. .. versionadded:: 2.2 + default_sort_order: Optional[:class:`ForumOrderType`] + The default sort order for posts in this forum channel. + + .. versionadded:: 2.3 """ __slots__ = ( @@ -2166,6 +2199,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): 'default_thread_slowmode_delay', 'default_reaction_emoji', 'default_layout', + 'default_sort_order', '_available_tags', '_flags', ) @@ -2211,6 +2245,11 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): 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._fill_overwrites(data) @@ -2337,6 +2376,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): default_thread_slowmode_delay: int = ..., default_reaction_emoji: Optional[EmojiInputType] = ..., default_layout: ForumLayoutType = ..., + default_sort_order: ForumOrderType = ..., require_tag: bool = ..., ) -> ForumChannel: ... @@ -2395,6 +2435,10 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): The new default layout for posts in this forum. .. 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` 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 + 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) if payload is not None: # the payload will always be the proper channel payload diff --git a/discord/client.py b/discord/client.py index c53b0f3f3..298959b21 100644 --- a/discord/client.py +++ b/discord/client.py @@ -1161,9 +1161,9 @@ class Client: 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, - ) -> Tuple[Interaction[Self], Union[Command, ContextMenu]]: + ) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]: ... # AutoMod @@ -1816,9 +1816,9 @@ class Client: event: Literal["command", "command_completion"], /, *, - check: Optional[Callable[[Context], bool]] = None, + check: Optional[Callable[[Context[Any]], bool]] = None, timeout: Optional[float] = None, - ) -> Context: + ) -> Context[Any]: ... @overload @@ -1827,9 +1827,9 @@ class Client: event: Literal["command_error"], /, *, - check: Optional[Callable[[Context, CommandError], bool]] = None, + check: Optional[Callable[[Context[Any], CommandError], bool]] = None, timeout: Optional[float] = None, - ) -> Tuple[Context, CommandError]: + ) -> Tuple[Context[Any], CommandError]: ... @overload @@ -2486,8 +2486,6 @@ class Client: The bot's application information. """ data = await self.http.application_info() - if 'rpc_origins' not in data: - data['rpc_origins'] = None return AppInfo(self._connection, data) async def fetch_user(self, user_id: int, /) -> User: diff --git a/discord/colour.py b/discord/colour.py index 6a9f7b8d5..52dca9cc0 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -104,6 +104,11 @@ class Colour: 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 ------------ value: :class:`int` diff --git a/discord/enums.py b/discord/enums.py index 8fcadf1eb..e0db03669 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -67,6 +67,7 @@ __all__ = ( 'AutoModRuleEventType', 'AutoModRuleActionType', 'ForumLayoutType', + 'ForumOrderType', 'OnboardingPromptType', ) @@ -235,11 +236,11 @@ class MessageType(Enum): auto_moderation_action = 24 role_subscription_purchase = 25 interaction_premium_upsell = 26 - # stage_start = 27 - # stage_end = 28 - # stage_speaker = 29 - # stage_raise_hand = 30 - # stage_topic = 31 + stage_start = 27 + stage_end = 28 + stage_speaker = 29 + stage_raise_hand = 30 + stage_topic = 31 guild_application_premium_subscription = 32 @@ -752,6 +753,11 @@ class ForumLayoutType(Enum): gallery_view = 2 +class ForumOrderType(Enum): + latest_activity = 0 + creation_date = 1 + + class OnboardingPromptType(Enum): multiple_choice = 0 dropdown = 1 diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 11f6a0393..02136bce8 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -50,6 +50,7 @@ from ._types import _BaseCommand, BotT if TYPE_CHECKING: from typing_extensions import Self from discord.abc import Snowflake + from discord._types import ClientT from .bot import BotBase from .context import Context @@ -585,6 +586,18 @@ class Cog(metaclass=CogMeta): """ 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 async def cog_command_error(self, ctx: Context[BotT], error: Exception) -> None: """|coro| diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 8c1f7212d..92a1a6b51 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations 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.utils @@ -615,6 +615,90 @@ class Context(discord.abc.Messageable, Generic[BotT]): except CommandError as 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: """|coro| @@ -716,6 +800,90 @@ class Context(discord.abc.Messageable, Generic[BotT]): if self.interaction: 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( self, content: Optional[str] = None, diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index cc7a3eb9b..e5be9d47f 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1330,7 +1330,7 @@ async def run_converters(ctx: Context[BotT], converter: Any, argument: str, para return value # 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 # Nearly every type is a generic type within the typing library diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 17dd49830..b25e6ae95 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -920,12 +920,17 @@ class BadLiteralArgument(UserInputError): A tuple of values compared against in conversion, in order of failure. errors: List[:class:`CommandError`] 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.literals: Tuple[Any, ...] = literals self.errors: List[CommandError] = errors + self.argument: str = argument to_string = [repr(l) for l in literals] if len(to_string) > 2: diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index fb6d39b31..41d9ffd25 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -72,9 +72,9 @@ __all__ = ( T = TypeVar('T') U = TypeVar('U') CogT = TypeVar('CogT', bound='Cog') -CommandT = TypeVar('CommandT', bound='Command') +CommandT = TypeVar('CommandT', bound='Command[Any, ..., Any]') # CHT = TypeVar('CHT', bound='Check') -GroupT = TypeVar('GroupT', bound='Group') +GroupT = TypeVar('GroupT', bound='Group[Any, ..., Any]') _NoneType = type(None) if TYPE_CHECKING: @@ -297,7 +297,7 @@ def replace_parameters( 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) params = replace_parameters(wrapped.params, wrapped.callback, signature) wrapped.callback.__signature__ = signature.replace(parameters=params) @@ -312,7 +312,7 @@ class HybridAppCommand(discord.app_commands.Command[CogT, P, T]): finally: 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 # This technically means only one flag converter is supported self.flag_converter: Optional[Tuple[str, Type[FlagConverter]]] = getattr( @@ -908,6 +908,9 @@ def hybrid_group( 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` Whether to register the command also as an application command. diff --git a/discord/gateway.py b/discord/gateway.py index a06195307..162217576 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -915,6 +915,7 @@ class DiscordVoiceWebSocket: 'd': { 'speaking': int(state), 'delay': 0, + 'ssrc': self._connection.ssrc, }, } @@ -948,16 +949,16 @@ class DiscordVoiceWebSocket: state.voice_port = data['port'] 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, 2, 70) # 70 = Length struct.pack_into('>I', packet, 4, state.ssrc) state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) - recv = await self.loop.sock_recv(state.socket, 70) + recv = await self.loop.sock_recv(state.socket, 74) _log.debug('received packet in initial_connection: %s', recv) - # the ip is ascii starting at the 4th byte and ending at the first null - ip_start = 4 + # the ip is ascii starting at the 8th byte and ending at the first null + ip_start = 8 ip_end = recv.index(0, ip_start) 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: _log.debug('received secret key for voice connection') 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) async def poll_event(self) -> None: diff --git a/discord/guild.py b/discord/guild.py index 32a7416ec..5593a88e5 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -73,6 +73,8 @@ from .enums import ( MFALevel, Locale, AutoModRuleEventType, + ForumOrderType, + ForumLayoutType, ) from .mixins import Hashable from .user import User @@ -91,6 +93,7 @@ from .object import OLDEST_OBJECT, Object from .onboarding import Onboarding from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction +from .partial_emoji import _EmojiTag, PartialEmoji __all__ = ( @@ -130,6 +133,7 @@ if TYPE_CHECKING: from .types.integration import IntegrationType from .types.snowflake import SnowflakeList from .types.widget import EditWidgetSettings + from .message import EmojiInputType VocalGuildChannel = Union[VoiceChannel, StageChannel] GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] @@ -290,6 +294,7 @@ class Guild(Hashable): 'mfa_level', 'vanity_url_code', 'widget_enabled', + '_widget_channel_id', '_members', '_channels', '_icon', @@ -478,6 +483,7 @@ class Guild(Hashable): self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0 self.vanity_url_code: Optional[str] = guild.get('vanity_url_code') 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.preferred_locale: Locale = try_enum(Locale, guild.get('preferred_locale', 'en-US')) self._discovery_splash: Optional[str] = guild.get('discovery_splash') @@ -737,6 +743,26 @@ class Guild(Hashable): """ 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 def system_channel(self) -> Optional[TextChannel]: """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 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 def emoji_limit(self) -> int: """:class:`int`: The maximum number of emoji slots this guild has.""" @@ -1200,6 +1238,7 @@ class Guild(Hashable): nsfw: bool = MISSING, overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, default_auto_archive_duration: int = MISSING, + default_thread_slowmode_delay: int = MISSING, ) -> TextChannel: """|coro| @@ -1273,6 +1312,10 @@ class Guild(Hashable): Must be one of ``60``, ``1440``, ``4320``, or ``10080``. .. 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`] The reason for creating this channel. Shows up on the audit log. @@ -1305,7 +1348,10 @@ class Guild(Hashable): options['nsfw'] = nsfw 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( name, @@ -1577,6 +1623,9 @@ class Guild(Hashable): reason: Optional[str] = None, default_auto_archive_duration: 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, ) -> ForumChannel: """|coro| @@ -1594,6 +1643,10 @@ class Guild(Hashable): ----------- name: :class:`str` 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` The channel's topic. category: Optional[:class:`CategoryChannel`] @@ -1617,6 +1670,19 @@ class Guild(Hashable): The default slowmode delay in seconds for threads created in this forum. .. 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`] The available tags for this forum channel. @@ -1656,6 +1722,30 @@ class Guild(Hashable): if default_thread_slowmode_delay is not MISSING: 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: options['available_tags'] = [t.to_dict() for t in available_tags] @@ -1728,6 +1818,9 @@ class Guild(Hashable): premium_progress_bar_enabled: bool = MISSING, discoverable: bool = MISSING, invites_disabled: bool = MISSING, + widget_enabled: bool = MISSING, + widget_channel: Optional[Snowflake] = MISSING, + mfa_level: MFALevel = MISSING, ) -> Guild: r"""|coro| @@ -1735,12 +1828,6 @@ class Guild(Hashable): 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 The newly updated guild is returned. @@ -1751,15 +1838,6 @@ class Guild(Hashable): This function will now raise :exc:`TypeError` or :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 ---------- name: :class:`str` @@ -1785,9 +1863,13 @@ class Guild(Hashable): Only PNG/JPEG supported. Could be ``None`` to denote removing the splash. This is only available to guilds that contain ``DISCOVERABLE`` in :attr:`Guild.features`. + + .. versionadded:: 2.0 community: :class:`bool` Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel`` and ``public_updates_channel`` parameters are required. + + .. versionadded:: 2.0 afk_channel: Optional[:class:`VoiceChannel`] The new channel that is the AFK channel. Could be ``None`` for no AFK channel. afk_timeout: :class:`int` @@ -1809,20 +1891,47 @@ class Guild(Hashable): The new system channel settings to use with the new system channel. preferred_locale: :class:`Locale` 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`] 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 channel. + + .. versionadded:: 1.4 public_updates_channel: Optional[:class:`TextChannel`] 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 public updates channel. + + .. versionadded:: 1.4 premium_progress_bar_enabled: :class:`bool` Whether the premium AKA server boost level progress bar should be enabled for the guild. + + .. versionadded:: 2.0 discoverable: :class:`bool` Whether server discovery is enabled for this guild. + + .. versionadded:: 2.1 invites_disabled: :class:`bool` 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`] 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. TypeError 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. Returns @@ -1974,6 +2083,21 @@ class Guild(Hashable): if premium_progress_bar_enabled is not MISSING: 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) return Guild(data=data, state=self._state) @@ -2783,7 +2907,7 @@ class Guild(Hashable): Parameters ------------ - id: :class:`int` + scheduled_event_id: :class:`int` The scheduled event ID. with_counts: :class:`bool` 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) 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( self, *, @@ -2847,6 +3033,8 @@ class Guild(Hashable): datetime object. Consider using :func:`utils.utcnow`. Required if the entity type is :attr:`EntityType.external`. + privacy_level: :class:`PrivacyLevel` + The privacy level of the scheduled event. entity_type: :class:`EntityType` The entity type of the scheduled event. If the channel is a :class:`StageInstance` or :class:`VoiceChannel` then this is @@ -2894,27 +3082,32 @@ class Guild(Hashable): ) 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 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 - else: - _entity_type = getattr(channel, '_scheduled_event_entity_type', MISSING) - if _entity_type is None: - 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') + else: + if not isinstance(entity_type, EntityType): + raise TypeError('entity_type must be of type EntityType') - entity_type = _entity_type + payload['entity_type'] = entity_type.value - if not isinstance(entity_type, EntityType): - raise TypeError('entity_type must be of type EntityType') + if entity_type is None: + 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: payload['description'] = description @@ -2924,7 +3117,7 @@ class Guild(Hashable): payload['image'] = image_as_str 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') payload['channel_id'] = channel.id @@ -2940,12 +3133,15 @@ class Guild(Hashable): metadata['location'] = location - if end_time is not MISSING: - if end_time.tzinfo is None: - 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 end_time in (MISSING, None): + raise TypeError('end_time must be set when entity_type is external') + + if end_time not in (MISSING, None): + if end_time.tzinfo is None: + 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: payload['entity_metadata'] = metadata @@ -3631,7 +3827,7 @@ class Guild(Hashable): if limit is not None: limit -= len(entries) - after = Object(id=int(entries[0]['id'])) + after = Object(id=int(entries[-1]['id'])) return data, entries, after, limit @@ -3648,20 +3844,19 @@ class Guild(Hashable): if isinstance(after, datetime.datetime): after = Object(id=utils.time_snowflake(after, high=True)) - if oldest_first is MISSING: - reverse = after is not MISSING - else: - reverse = oldest_first + if oldest_first: + if after is MISSING: + after = OLDEST_OBJECT predicate = None - if reverse: + if oldest_first: strategy, state = _after_strategy, after if before: predicate = lambda m: int(m['id']) < before.id else: strategy, state = _before_strategy, before - if after and after != OLDEST_OBJECT: + if after: predicate = lambda m: int(m['id']) > after.id # avoid circular import @@ -3674,8 +3869,6 @@ class Guild(Hashable): data, raw_entries, state, limit = await strategy(retrieve, state, limit) - if reverse: - raw_entries = reversed(raw_entries) if predicate: raw_entries = filter(predicate, raw_entries) @@ -3748,7 +3941,7 @@ class Guild(Hashable): ) -> None: """|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. @@ -3776,7 +3969,8 @@ class Guild(Hashable): if enabled is not MISSING: 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]: """|coro| @@ -3968,7 +4162,7 @@ class Guild(Hashable): event_type: AutoModRuleEventType, trigger: AutoModTrigger, actions: List[AutoModRuleAction], - enabled: bool = MISSING, + enabled: bool = False, exempt_roles: Sequence[Snowflake] = MISSING, exempt_channels: Sequence[Snowflake] = MISSING, reason: str = MISSING, @@ -3993,7 +4187,7 @@ class Guild(Hashable): The actions that will be taken when the automod rule is triggered. enabled: :class:`bool` Whether the automod rule is enabled. - Discord will default to ``False``. + Defaults to ``False``. exempt_roles: Sequence[:class:`abc.Snowflake`] A list of roles that will be exempt from the automod rule. exempt_channels: Sequence[:class:`abc.Snowflake`] diff --git a/discord/http.py b/discord/http.py index 70ad63191..e3c053b27 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1149,6 +1149,7 @@ class HTTPClient: 'available_tags', 'applied_tags', 'default_forum_layout', + 'default_sort_order', ) payload = {k: v for k, v in options.items() if k in valid_keys} @@ -1190,6 +1191,9 @@ class HTTPClient: 'video_quality_mode', 'default_auto_archive_duration', 'default_thread_rate_limit_per_user', + 'default_sort_order', + 'default_reaction_emoji', + 'default_forum_layout', 'available_tags', ) 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) + 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]: return self.request(Route('GET', '/guilds/templates/{code}', code=code)) @@ -1718,7 +1728,7 @@ class HTTPClient: params: Dict[str, Any] = {'limit': limit} if before: params['before'] = before - if after: + if after is not None: params['after'] = after if user_id: params['user_id'] = user_id @@ -1904,6 +1914,7 @@ class HTTPClient: 'channel_id', 'topic', 'privacy_level', + 'send_start_notification', ) payload = {k: v for k, v in payload.items() if k in valid_keys} diff --git a/discord/interactions.py b/discord/interactions.py index d2b900d3d..f9ed7976d 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -195,6 +195,11 @@ class Interaction(Generic[ClientT]): if 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: member = data['member'] # type: ignore # The key is optional and handled except KeyError: diff --git a/discord/message.py b/discord/message.py index 06cd9e3af..8c9b732f5 100644 --- a/discord/message.py +++ b/discord/message.py @@ -60,7 +60,7 @@ from .utils import escape_mentions, MISSING from .http import handle_message_parameters from .guild import Guild from .mixins import Hashable -from .sticker import StickerItem +from .sticker import StickerItem, GuildSticker from .threads import Thread from .channel import PartialMessageable @@ -700,6 +700,7 @@ class PartialMessage(Hashable): - :meth:`TextChannel.get_partial_message` - :meth:`VoiceChannel.get_partial_message` + - :meth:`StageChannel.get_partial_message` - :meth:`Thread.get_partial_message` - :meth:`DMChannel.get_partial_message` @@ -723,7 +724,7 @@ class PartialMessage(Hashable): 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. id: :class:`int` The message ID. @@ -737,6 +738,7 @@ class PartialMessage(Hashable): if not isinstance(channel, PartialMessageable) and channel.type not in ( ChannelType.text, ChannelType.voice, + ChannelType.stage_voice, ChannelType.news, ChannelType.private, ChannelType.news_thread, @@ -744,7 +746,7 @@ class PartialMessage(Hashable): ChannelType.private_thread, ): 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 @@ -1252,6 +1254,86 @@ class PartialMessage(Hashable): ) 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: """|coro| @@ -1357,7 +1439,7 @@ class Message(PartialMessage, Hashable): A list of embeds the message has. 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. - 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. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. 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' 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 return '' diff --git a/discord/permissions.py b/discord/permissions.py index 74c7173e4..9716f07c6 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -177,7 +177,7 @@ class Permissions(BaseFlags): """A factory method that creates a :class:`Permissions` with all permissions set to ``True``. """ - return cls(0b11111111111111111111111111111111111111111) + return cls(0b1111111111111111111111111111111111111111111111) @classmethod def _timeout_mask(cls) -> int: @@ -204,7 +204,7 @@ class Permissions(BaseFlags): ``True`` and the guild-specific ones set to ``False``. The guild-specific permissions are currently: - - :attr:`manage_emojis` + - :attr:`manage_guild_expressions` - :attr:`view_audit_log` - :attr:`view_guild_insights` - :attr:`manage_guild` @@ -221,8 +221,11 @@ class Permissions(BaseFlags): Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`, :attr:`use_external_stickers`, :attr:`send_messages_in_threads` and :attr:`request_to_speak` permissions. + + .. versionchanged:: 2.3 + Added :attr:`use_soundboard` """ - return cls(0b111110110110011111101111111111101010001) + return cls(0b1000111110110110011111101111111111101010001) @classmethod def general(cls) -> Self: @@ -265,7 +268,7 @@ class Permissions(BaseFlags): def voice(cls) -> Self: """A factory method that creates a :class:`Permissions` with all "Voice" permissions from the official Discord UI set to ``True``.""" - return cls(0b1000000000000011111100000000001100000000) + return cls(0b1001001000000000000011111100000000001100000000) @classmethod def stage(cls) -> Self: @@ -305,7 +308,7 @@ class Permissions(BaseFlags): - :attr:`manage_messages` - :attr:`manage_roles` - :attr:`manage_webhooks` - - :attr:`manage_emojis_and_stickers` + - :attr:`manage_guild_expressions` - :attr:`manage_threads` - :attr:`moderate_members` @@ -544,13 +547,21 @@ class Permissions(BaseFlags): return 1 << 29 @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: - """: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 - @make_permission_alias('manage_emojis') + @make_permission_alias('manage_guild_expressions') 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 """ @@ -644,6 +655,22 @@ class Permissions(BaseFlags): """ 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): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -745,6 +772,7 @@ class PermissionOverwrite: manage_roles: Optional[bool] manage_permissions: Optional[bool] manage_webhooks: Optional[bool] + manage_guild_expressions: Optional[bool] manage_emojis: Optional[bool] manage_emojis_and_stickers: Optional[bool] use_application_commands: Optional[bool] @@ -758,6 +786,8 @@ class PermissionOverwrite: use_external_stickers: Optional[bool] use_embedded_activities: Optional[bool] moderate_members: Optional[bool] + use_soundboard: Optional[bool] + use_external_sounds: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/scheduled_event.py b/discord/scheduled_event.py index 9f8bd9920..f74ae6706 100644 --- a/discord/scheduled_event.py +++ b/discord/scheduled_event.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations 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 .enums import EventStatus, EntityType, PrivacyLevel, try_enum @@ -298,6 +298,87 @@ class ScheduledEvent(Hashable): 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( self, *, @@ -414,24 +495,34 @@ class ScheduledEvent(Hashable): payload['image'] = image_as_str entity_type = entity_type or getattr(channel, '_scheduled_event_entity_type', MISSING) - if entity_type is None: - raise TypeError( - f'invalid GuildChannel type passed, must be VoiceChannel or StageChannel not {channel.__class__.__name__}' - ) - - if entity_type is not MISSING: + if entity_type 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 + else: if not isinstance(entity_type, EntityType): raise TypeError('entity_type must be of type EntityType') 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_changed = _entity_type is not self.entity_type if _entity_type in (EntityType.stage_instance, EntityType.voice): if channel is MISSING or channel is None: - raise TypeError('channel must be set when entity_type is voice or stage_instance') - - payload['channel_id'] = channel.id + if _entity_type_changed: + raise TypeError('channel must be set when entity_type is voice or stage_instance') + else: + payload['channel_id'] = channel.id if location not in (MISSING, None): 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 if location is MISSING or location is None: - raise TypeError('location must be set when entity_type is external') - - metadata['location'] = location + if _entity_type_changed: + raise TypeError('location must be set when entity_type is external') + 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') if end_time is not MISSING: diff --git a/discord/sticker.py b/discord/sticker.py index 2872f3663..225e7648a 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -414,7 +414,7 @@ class GuildSticker(Sticker): def _from_data(self, data: GuildStickerPayload) -> None: super()._from_data(data) - self.available: bool = data['available'] + self.available: bool = data.get('available', True) self.guild_id: int = int(data['guild_id']) user = data.get('user') self.user: Optional[User] = self._state.store_user(user) if user else None diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index 640fe1be7..87bafc6d2 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -44,10 +44,14 @@ class BaseAppInfo(TypedDict): icon: Optional[str] summary: 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): - rpc_origins: List[str] owner: User bot_public: bool bot_require_code_grant: bool @@ -55,8 +59,6 @@ class AppInfo(BaseAppInfo): guild_id: NotRequired[Snowflake] primary_sku_id: NotRequired[Snowflake] slug: NotRequired[str] - terms_of_service_url: NotRequired[str] - privacy_policy_url: NotRequired[str] hook: NotRequired[bool] max_participants: NotRequired[int] tags: NotRequired[List[str]] @@ -66,13 +68,12 @@ class AppInfo(BaseAppInfo): class PartialAppInfo(BaseAppInfo, total=False): - rpc_origins: List[str] - cover_image: str hook: bool - terms_of_service_url: str - privacy_policy_url: str 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): diff --git a/discord/types/automod.py b/discord/types/automod.py index 250d1e41e..9b0b13692 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -45,9 +45,13 @@ class _AutoModerationActionMetadataTimeout(TypedDict): duration_seconds: int +class _AutoModerationActionMetadataCustomMessage(TypedDict): + custom_message: str + + class _AutoModerationActionBlockMessage(TypedDict): type: Literal[1] - metadata: NotRequired[Empty] + metadata: NotRequired[_AutoModerationActionMetadataCustomMessage] class _AutoModerationActionAlert(TypedDict): diff --git a/discord/types/channel.py b/discord/types/channel.py index ad17af689..421232b45 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -134,6 +134,7 @@ class ForumTag(TypedDict): emoji_name: Optional[str] +ForumOrderType = Literal[0, 1] ForumLayoutType = Literal[0, 1, 2] @@ -141,6 +142,7 @@ class ForumChannel(_BaseTextChannel): type: Literal[15] available_tags: List[ForumTag] default_reaction_emoji: Optional[DefaultReaction] + default_sort_order: Optional[ForumOrderType] default_forum_layout: NotRequired[ForumLayoutType] flags: NotRequired[int] diff --git a/discord/types/guild.py b/discord/types/guild.py index b6c2a1365..1ff2854aa 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -161,6 +161,10 @@ class GuildPrune(TypedDict): pruned: Optional[int] +class GuildMFALevel(TypedDict): + level: MFALevel + + class ChannelPositionUpdate(TypedDict): id: Snowflake position: Optional[int] diff --git a/discord/types/sticker.py b/discord/types/sticker.py index 7dcd0ccba..15fd034a7 100644 --- a/discord/types/sticker.py +++ b/discord/types/sticker.py @@ -55,7 +55,7 @@ class StandardSticker(BaseSticker): class GuildSticker(BaseSticker): type: Literal[2] - available: bool + available: NotRequired[bool] guild_id: Snowflake user: NotRequired[User] diff --git a/discord/ui/item.py b/discord/ui/item.py index 443876c1a..2ef42fb9e 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -40,7 +40,7 @@ if TYPE_CHECKING: from .view import View from ..components import Component -I = TypeVar('I', bound='Item') +I = TypeVar('I', bound='Item[Any]') V = TypeVar('V', bound='View', covariant=True) ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] diff --git a/discord/ui/select.py b/discord/ui/select.py index 11555c7f7..bcd7b466d 100644 --- a/discord/ui/select.py +++ b/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. """ 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 import inspect import os @@ -71,12 +71,12 @@ if TYPE_CHECKING: ] V = TypeVar('V', bound='View', covariant=True) -BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect') -SelectT = TypeVar('SelectT', bound='Select') -UserSelectT = TypeVar('UserSelectT', bound='UserSelect') -RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect') -ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect') -MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect') +BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]') +SelectT = TypeVar('SelectT', bound='Select[Any]') +UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]') +RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]') +ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]') +MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]') SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] selected_values: ContextVar[Dict[str, List[PossibleValue]]] = ContextVar('selected_values') diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 3128246f2..71564a58d 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -636,9 +636,9 @@ class SyncWebhook(BaseWebhook): Returns -------- - :class:`Webhook` - A partial :class:`Webhook`. - A partial webhook is just a webhook object with an ID and a token. + :class:`SyncWebhook` + A partial :class:`SyncWebhook`. + A partial :class:`SyncWebhook` is just a :class:`SyncWebhook` object with an ID and a token. """ data: WebhookPayload = { 'id': id, @@ -678,9 +678,9 @@ class SyncWebhook(BaseWebhook): Returns -------- - :class:`Webhook` - A partial :class:`Webhook`. - A partial webhook is just a webhook object with an ID and a token. + :class:`SyncWebhook` + A partial :class:`SyncWebhook`. + 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[0-9]{17,20})/(?P[A-Za-z0-9\.\-\_]{60,68})', url) if m is None: diff --git a/discord/widget.py b/discord/widget.py index 2c46d49ba..2a7c17a21 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -226,7 +226,7 @@ class Widget: The guild's name. channels: List[:class:`WidgetChannel`] The accessible voice channels in the guild. - members: List[:class:`Member`] + members: List[:class:`WidgetMember`] The online members in the guild. Offline members do not appear in the widget. diff --git a/docs/api.rst b/docs/api.rst index 4f64a0c9d..adc523680 100644 --- a/docs/api.rst +++ b/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 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 .. attribute:: guild_application_premium_subscription @@ -1950,6 +1975,8 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.verification_level` - :attr:`~AuditLogDiff.widget_channel` - :attr:`~AuditLogDiff.widget_enabled` + - :attr:`~AuditLogDiff.premium_progress_bar_enabled` + - :attr:`~AuditLogDiff.system_channel_flags` .. attribute:: channel_create @@ -1991,6 +2018,9 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.rtc_region` - :attr:`~AuditLogDiff.video_quality_mode` - :attr:`~AuditLogDiff.default_auto_archive_duration` + - :attr:`~AuditLogDiff.nsfw` + - :attr:`~AuditLogDiff.slowmode_delay` + - :attr:`~AuditLogDiff.user_limit` .. attribute:: channel_delete @@ -2007,6 +2037,9 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.type` - :attr:`~AuditLogDiff.overwrites` + - :attr:`~AuditLogDiff.flags` + - :attr:`~AuditLogDiff.nsfw` + - :attr:`~AuditLogDiff.slowmode_delay` .. attribute:: overwrite_create @@ -2078,7 +2111,7 @@ of :class:`enum.Enum`. When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with two attributes: - - ``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. 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. +.. 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: Audit Log Data @@ -3926,6 +3974,42 @@ AuditLogDiff :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 I'm not sure how to port these diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index 6420d2b0d..9bda24f6e 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -256,7 +256,7 @@ GroupCog .. attributetable:: discord.ext.commands.GroupCog .. autoclass:: discord.ext.commands.GroupCog - :members: + :members: interaction_check CogMeta diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index e479c4b08..02a9ae670 100644 --- a/docs/ext/commands/commands.rst +++ b/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 commands.check(predicate) + @bot.command() @guild_only() async def test(ctx): await ctx.send('Hey this is not a DM! Nice.') diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 614312c8a..760be0152 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,96 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed 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 ` to not prematurely close (:issue:`9257`) + .. _vp2p1p1: v2.1.1