From ac641ea98b26b025f0afc08a42584158c79c3ac7 Mon Sep 17 00:00:00 2001 From: dolfies Date: Wed, 4 Dec 2024 03:53:14 -0500 Subject: [PATCH] Normalize upstream changes for the library --- discord/audit_logs.py | 1 - discord/client.py | 47 +++++++++++++------ discord/flags.py | 3 +- discord/gateway.py | 15 +++--- discord/guild.py | 82 ++++++++++++++++++++++++-------- discord/http.py | 22 ++++++--- discord/message.py | 80 ++++++++++++++++++++++---------- discord/permissions.py | 11 ++--- discord/player.py | 2 +- discord/raw_models.py | 2 + discord/reaction.py | 23 ++++++--- discord/state.py | 11 +++-- discord/types/channel.py | 10 +++- discord/types/guild.py | 7 +++ discord/types/message.py | 4 +- discord/types/poll.py | 2 +- discord/types/voice.py | 2 +- discord/voice_client.py | 23 ++++++--- discord/voice_state.py | 98 +++++++++++++++++++++++++-------------- discord/webhook/async_.py | 3 +- docs/api.rst | 45 +++++++++--------- 21 files changed, 323 insertions(+), 170 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 402b10bdc..4726d1131 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -70,7 +70,6 @@ if TYPE_CHECKING: from .types.invite import Invite as InvitePayload from .types.role import Role as RolePayload from .types.snowflake import Snowflake - from .types.command import ApplicationCommandPermissions from .types.automod import AutoModerationAction from .user import User from .webhook import Webhook diff --git a/discord/client.py b/discord/client.py index 49651d7e0..4874a03bf 100644 --- a/discord/client.py +++ b/discord/client.py @@ -274,6 +274,11 @@ class Client: set to is ``30.0`` seconds. .. versionadded:: 2.0 + preferred_rtc_regions: List[:class:`str`] + + A list of preferred RTC regions to connect to. This overrides Discord's suggested list. + + .. versionadded:: 2.1 Attributes ----------- @@ -533,13 +538,24 @@ class Client: def preferred_rtc_regions(self) -> List[str]: """List[:class:`str`]: Geo-ordered list of voice regions the connected client can use. + This value is determined by Discord by default, but can be set to override it. + .. versionadded:: 2.0 .. versionchanged:: 2.1 Rename from ``preferred_voice_regions`` to ``preferred_rtc_regions``. """ - return self._connection.preferred_rtc_regions + return ( + self._connection.overriden_rtc_regions + if self._connection.overriden_rtc_regions is not None + else self._connection.preferred_rtc_regions + ) + + @preferred_rtc_regions.setter + def preferred_rtc_regions(self, value: List[str]) -> None: + values = [str(x).lower() for x in value] + self._connection.overriden_rtc_regions = values @property def pending_payments(self) -> Sequence[Payment]: @@ -1808,7 +1824,6 @@ class Client: self_mute: bool = False, self_deaf: bool = False, self_video: bool = False, - preferred_region: Optional[str] = MISSING, ) -> None: """|coro| @@ -1816,6 +1831,10 @@ class Client: .. versionadded:: 2.0 + .. versionchanged:: 2.1 + + Removed the ``preferred_region`` parameter. + Parameters ----------- channel: Optional[:class:`~discord.abc.Snowflake`] @@ -1827,19 +1846,12 @@ class Client: self_video: :class:`bool` Indicates if the client is using video. Untested & unconfirmed (do not use). - preferred_region: Optional[:class:`str`] - The preferred region to connect to. """ state = self._connection ws = self.ws channel_id = channel.id if channel else None - if preferred_region is None or channel_id is None: - region = None - else: - region = str(preferred_region) if preferred_region else state.preferred_rtc_region - - await ws.voice_state(None, channel_id, self_mute, self_deaf, self_video, preferred_region=region) + await ws.voice_state(None, channel_id, self_mute, self_deaf, self_video) # Guild stuff @@ -3092,13 +3104,18 @@ class Client: data = await self.http.get_country_code() return data['country_code'] - async def fetch_preferred_voice_regions(self) -> List[str]: + async def fetch_preferred_rtc_regions(self) -> List[Tuple[str, List[str]]]: """|coro| - Retrieves the preferred voice regions of the client. + Retrieves the preferred RTC regions of the client. .. versionadded:: 2.0 + .. versionchanged:: 2.1 + + Changed the name of the method from ``fetch_preferred_voice_regions`` to ``fetch_preferred_rtc_regions``. + The method now returns a list of tuples instead of a list of strings. + Raises ------- HTTPException @@ -3106,11 +3123,11 @@ class Client: Returns ------- - List[:class:`str`] - The preferred voice regions of the client. + List[Tuple[:class:`str`, List[:class:`str`]]] + The region name and list of IPs for the closest voice regions. """ data = await self.http.get_preferred_voice_regions() - return [v['region'] for v in data] + return [(v['region'], v['ips']) for v in data] async def create_dm(self, user: Snowflake, /) -> DMChannel: """|coro| diff --git a/discord/flags.py b/discord/flags.py index b6707432d..4af1ed087 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -2750,8 +2750,6 @@ class AttachmentFlags(BaseFlags): class RoleFlags(BaseFlags): r"""Wraps up the Discord Role flags - .. versionadded:: 2.1 - .. container:: operations .. describe:: x == y @@ -2795,6 +2793,7 @@ class RoleFlags(BaseFlags): Returns whether any flag is set to ``True``. + .. versionadded:: 2.1 Attributes ----------- diff --git a/discord/gateway.py b/discord/gateway.py index 2016e1a0a..8eee48f61 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -775,8 +775,6 @@ class DiscordWebSocket: self_mute: bool = False, self_deaf: bool = False, self_video: bool = False, - *, - preferred_region: Optional[str] = None, ) -> None: payload = { 'op': self.VOICE_STATE, @@ -789,8 +787,8 @@ class DiscordWebSocket: }, } - if preferred_region is not None: - payload['d']['preferred_region'] = preferred_region + if channel_id: + payload['d'].update(self._connection._get_preferred_regions()) _log.debug('Updating %s voice state to %s.', guild_id or 'client', payload) await self.send_as_json(payload) @@ -1025,7 +1023,6 @@ class DiscordVoiceWebSocket: await self.loop.sock_connect(state.socket, (state.endpoint_ip, state.voice_port)) state.ip, state.port = await self.discover_ip() - # there *should* always be at least one supported mode (xsalsa20_poly1305) modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes] _log.debug('Received supported encryption modes: %s.', ', '.join(modes)) @@ -1055,7 +1052,7 @@ class DiscordVoiceWebSocket: _log.debug('Received IP discovery packet: %s.', recv) - # the ip is ascii starting at the 8th byte and ending at the first null + # 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) ip = recv[ip_start:ip_end].decode('ascii') @@ -1084,9 +1081,9 @@ class DiscordVoiceWebSocket: _log.debug('Received secret key for voice connection.') self.secret_key = self._connection.secret_key = data['secret_key'] - # Send a speak command with the "not speaking" state. + # 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). + # 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: @@ -1098,7 +1095,7 @@ class DiscordVoiceWebSocket: _log.debug('Voice received %s.', msg) raise ConnectionClosed(self.ws) from msg.data elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING): - _log.debug('Voice received %s', msg) + _log.debug('Voice received %s.', msg) raise ConnectionClosed(self.ws, code=self._close_code) async def close(self, code: int = 1000) -> None: diff --git a/discord/guild.py b/discord/guild.py index 5fd3521d5..b9554aad6 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -377,9 +377,13 @@ class Guild(Hashable): max_members: Optional[:class:`int`] The maximum amount of members for the guild. max_video_channel_users: Optional[:class:`int`] - The maximum amount of users in a video channel. + The maximum amount of users in a video stream. .. versionadded:: 1.4 + max_stage_video_channel_users: Optional[:class:`int`] + The maximum amount of users in a stage video stream. + + .. versionadded:: 2.1 description: Optional[:class:`str`] The guild's description. verification_level: :class:`VerificationLevel` @@ -453,6 +457,7 @@ class Guild(Hashable): 'max_presences', 'max_members', 'max_video_channel_users', + 'max_stage_video_channel_users', '_premium_tier', 'premium_subscription_count', 'preferred_locale', @@ -478,6 +483,7 @@ class Guild(Hashable): '_discovery_splash', '_rules_channel_id', '_public_updates_channel_id', + '_safety_alerts_channel_id', '_stage_instances', '_scheduled_events', '_threads', @@ -671,6 +677,7 @@ class Guild(Hashable): self.max_presences: Optional[int] = guild.get('max_presences') self.max_members: Optional[int] = guild.get('max_members') self.max_video_channel_users: Optional[int] = guild.get('max_video_channel_users') + self.max_stage_video_channel_users: Optional[int] = guild.get('max_stage_video_channel_users') self._premium_tier = guild.get('premium_tier') self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0 self.vanity_url_code: Optional[str] = guild.get('vanity_url_code') @@ -681,6 +688,7 @@ class Guild(Hashable): self._discovery_splash: Optional[str] = guild.get('discovery_splash') self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id') self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id') + self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'safety_alerts_channel_id') self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0)) self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0)) @@ -747,6 +755,16 @@ class Guild(Hashable): def _extra_large(self) -> bool: return self._member_count is not None and self._member_count >= 75000 + @property + def max_stage_video_users(self) -> Optional[int]: + """Optional[:class:`int`]: The maximum amount of users in a stage video stream. + + An alias for :attr:`max_stage_video_channel_users`. + + .. versionadded:: 2.1 + """ + return self.max_stage_video_channel_users + def is_hub(self) -> bool: """:class:`bool`: Whether the guild is a Student Hub. @@ -1047,6 +1065,17 @@ class Guild(Hashable): channel_id = self._public_updates_channel_id return channel_id and self._channels.get(channel_id) # type: ignore + @property + def safety_alerts_channel(self) -> Optional[TextChannel]: + """Optional[:class:`TextChannel`]: Return's the guild's channel used for safety alerts, if set. + + For example, this is used for the raid protection setting. The guild must have the ``COMMUNITY`` feature. + + .. versionadded:: 2.1 + """ + channel_id = self._safety_alerts_channel_id + return channel_id and self._channels.get(channel_id) # type: ignore + @property def afk_channel(self) -> Optional[VocalGuildChannel]: """Optional[:class:`VoiceChannel`]: Returns the guild channel AFK users are moved to. @@ -2184,12 +2213,14 @@ class Guild(Hashable): preferred_locale: Locale = MISSING, rules_channel: Optional[TextChannel] = MISSING, public_updates_channel: Optional[TextChannel] = MISSING, + safety_alerts_channel: Optional[TextChannel] = MISSING, 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, + raid_alerts_disabled: bool = MISSING, invites_disabled_until: datetime = MISSING, dms_disabled_until: datetime = MISSING, ) -> Guild: @@ -2278,6 +2309,12 @@ class Guild(Hashable): public updates channel. .. versionadded:: 1.4 + safety_alerts_channel: Optional[:class:`TextChannel`] + The new channel that is used for safety alerts. This is only available to + guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no + safety alerts channel. + + .. versionadded:: 2.1 premium_progress_bar_enabled: :class:`bool` Whether the premium AKA server boost level progress bar should be enabled for the guild. @@ -2303,20 +2340,22 @@ class Guild(Hashable): Note that you must be owner of the guild to do this. .. versionadded:: 2.0 - reason: Optional[:class:`str`] - The reason for editing this guild. Shows up on the audit log. + raid_alerts_disabled: :class:`bool` + Whether the alerts for raid protection should be disabled for the guild. + .. versionadded:: 2.1 invites_disabled_until: Optional[:class:`datetime.datetime`] The time when invites should be enabled again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. .. versionadded:: 2.1 - dms_disabled_until: Optional[:class:`datetime.datetime`] The time when direct messages should be allowed again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. .. versionadded:: 2.1 + reason: Optional[:class:`str`] + The reason for editing this guild. Shows up on the audit log. Raises ------- @@ -2410,6 +2449,12 @@ class Guild(Hashable): else: fields['public_updates_channel_id'] = public_updates_channel.id + if safety_alerts_channel is not MISSING: + if safety_alerts_channel is None: + fields['safety_alerts_channel_id'] = safety_alerts_channel + else: + fields['safety_alerts_channel_id'] = safety_alerts_channel.id + if owner is not MISSING: if self.owner_id != self._state.self_id: raise ValueError('To transfer ownership you must be the owner of the guild') @@ -2434,7 +2479,7 @@ class Guild(Hashable): fields['system_channel_flags'] = system_channel_flags.value - if any(feat is not MISSING for feat in (community, discoverable, invites_disabled)): + if any(feat is not MISSING for feat in (community, discoverable, invites_disabled, raid_alerts_disabled)): features = set(self.features) if community is not MISSING: @@ -2460,6 +2505,12 @@ class Guild(Hashable): else: features.discard('INVITES_DISABLED') + if raid_alerts_disabled is not MISSING: + if raid_alerts_disabled: + features.add('RAID_ALERTS_DISABLED') + else: + features.discard('RAID_ALERTS_DISABLED') + fields['features'] = list(features) if premium_progress_bar_enabled is not MISSING: @@ -5212,6 +5263,10 @@ class Guild(Hashable): .. versionadded:: 1.4 + .. versionchanged:: 2.1 + + Removed the ``preferred_region`` parameter. + Parameters ----------- channel: Optional[:class:`abc.Snowflake`] @@ -5223,23 +5278,12 @@ class Guild(Hashable): self_video: :class:`bool` Indicates if the client is using video. Untested & unconfirmed (do not use). - preferred_region: Optional[:class:`str`] - The preferred region to connect to. - - .. versionchanged:: 2.0 - - The type of this parameter has changed to :class:`str`. """ state = self._state ws = state.ws channel_id = channel.id if channel else None - if preferred_region is None or channel_id is None: - region = None - else: - region = str(preferred_region) if preferred_region else state.preferred_rtc_region - - await ws.voice_state(self.id, channel_id, self_mute, self_deaf, self_video, preferred_region=region) + await ws.voice_state(self.id, channel_id, self_mute, self_deaf, self_video) async def subscribe( self, *, typing: bool = MISSING, activities: bool = MISSING, threads: bool = MISSING, member_updates: bool = MISSING @@ -5575,7 +5619,7 @@ class Guild(Hashable): return utils.parse_time(self._incidents_data.get('dms_disabled_until')) @property - def dm_spam_detected_at(self) -> Optional[datetime.datetime]: + def dm_spam_detected_at(self) -> Optional[datetime]: """:class:`datetime.datetime`: Returns the time when DM spam was detected in the guild. .. versionadded:: 2.1 @@ -5586,7 +5630,7 @@ class Guild(Hashable): return utils.parse_time(self._incidents_data.get('dm_spam_detected_at')) @property - def raid_detected_at(self) -> Optional[datetime.datetime]: + def raid_detected_at(self) -> Optional[datetime]: """Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild. .. versionadded:: 2.1 diff --git a/discord/http.py b/discord/http.py index c26a00059..00b12d62d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1053,7 +1053,7 @@ class HTTPClient: raise HTTPException(response, data) - async def get_preferred_voice_regions(self) -> List[dict]: + async def get_preferred_voice_regions(self) -> List[guild.RTCRegion]: async with self.__session.get('https://latency.discord.media/rtc') as resp: if resp.status == 200: return await resp.json() @@ -1283,7 +1283,10 @@ class HTTPClient: else: return self.request(r, json=params.payload) - def add_reaction(self, channel_id: Snowflake, message_id: Snowflake, emoji: str) -> Response[None]: + def add_reaction( + self, channel_id: Snowflake, message_id: Snowflake, emoji: str, type: message.ReactionType = 0 + ) -> Response[None]: + params = {'type': type} return self.request( Route( 'PUT', @@ -1291,31 +1294,36 @@ class HTTPClient: channel_id=channel_id, message_id=message_id, emoji=emoji, - ) + ), + params=params, ) def remove_reaction( - self, channel_id: Snowflake, message_id: Snowflake, emoji: str, member_id: Snowflake + self, channel_id: Snowflake, message_id: Snowflake, emoji: str, member_id: Snowflake, type: message.ReactionType = 0 ) -> Response[None]: return self.request( Route( 'DELETE', - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}', + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{reaction_type}/{member_id}', channel_id=channel_id, message_id=message_id, member_id=member_id, emoji=emoji, + reaction_type=type, ) ) - def remove_own_reaction(self, channel_id: Snowflake, message_id: Snowflake, emoji: str) -> Response[None]: + def remove_own_reaction( + self, channel_id: Snowflake, message_id: Snowflake, emoji: str, type: message.ReactionType = 0 + ) -> Response[None]: return self.request( Route( 'DELETE', - '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', + '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{reaction_type}/@me', channel_id=channel_id, message_id=message_id, emoji=emoji, + reaction_type=type, ) ) diff --git a/discord/message.py b/discord/message.py index 309a2ca89..593a3ff64 100644 --- a/discord/message.py +++ b/discord/message.py @@ -47,12 +47,22 @@ from typing import ( overload, ) +from discord.types.components import MessageActionRow + from . import utils from .reaction import Reaction from .emoji import Emoji from .partial_emoji import PartialEmoji from .calls import CallMessage -from .enums import MessageType, ChannelType, ApplicationCommandType, PurchaseNotificationType, MessageReferenceType, try_enum +from .enums import ( + MessageType, + ChannelType, + ApplicationCommandType, + PurchaseNotificationType, + MessageReferenceType, + ReactionType, + try_enum, +) from .errors import HTTPException from .components import _component_factory from .embeds import Embed @@ -472,13 +482,29 @@ class DeletedReferencedMessage: return self._parent.guild_id -class MessageSnapshot: +class MessageSnapshot(Hashable): """Represents a message snapshot attached to a forwarded message. + .. container:: operations + + .. describe:: x == y + + Checks if the message snapshot is equal to another message snapshot. + + .. describe:: x != y + + Checks if the message snapshot is not equal to another message snapshot. + + .. describe:: hash(x) + + Returns the hash of the message snapshot. + .. versionadded:: 2.1 Attributes ----------- + id: :class:`int` + The ID of the forwarded message. type: :class:`MessageType` The type of the forwarded message. content: :class:`str` @@ -519,27 +545,29 @@ class MessageSnapshot: cls, state: ConnectionState, message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]], + reference: MessageReference, ) -> List[Self]: if not message_snapshots: return [] - return [cls(state, snapshot['message']) for snapshot in message_snapshots] + return [cls(state, snapshot['message'], reference) for snapshot in message_snapshots] - def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): + def __init__(self, state: ConnectionState, data: MessageSnapshotPayload, reference: MessageReference): self.type: MessageType = try_enum(MessageType, data['type']) + self.id: int = reference.message_id # type: ignore self.content: str = data['content'] self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']] self.created_at: datetime.datetime = utils.parse_time(data['timestamp']) self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) - self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('stickers_items', [])] + self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] - self.components: List[MessageComponentType] = [] + self.components: List[MessageActionRow] = [] for component_data in data.get('components', []): component = _component_factory(component_data) if component is not None: - self.components.append(component) + self.components.append(component) # type: ignore self._state: ConnectionState = state @@ -577,16 +605,7 @@ class MessageSnapshot: state = self._state return ( utils.find( - lambda m: ( - m.created_at == self.created_at - and m.edited_at == self.edited_at - and m.content == self.content - and m.embeds == self.embeds - and m.components == self.components - and m.stickers == self.stickers - and m.attachments == self.attachments - and m.flags == self.flags - ), + lambda m: m.id == self.id, reversed(state._messages), ) if state._messages @@ -1200,7 +1219,7 @@ class PartialMessage(Hashable): # pinned exists on PartialMessage for duck typing purposes self.pinned = False - async def add_reaction(self, emoji: Union[EmojiInputType, Reaction], /) -> None: + async def add_reaction(self, emoji: Union[EmojiInputType, Reaction], /, *, boost: bool = False) -> None: """|coro| Adds a reaction to the message. @@ -1223,6 +1242,10 @@ class PartialMessage(Hashable): ------------ emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] The emoji to react with. + boost: :class:`bool` + Whether to react with a super reaction. + + .. versionadded:: 2.1 Raises -------- @@ -1236,9 +1259,9 @@ class PartialMessage(Hashable): The emoji parameter is invalid. """ emoji = convert_emoji_reaction(emoji) - await self._state.http.add_reaction(self.channel.id, self.id, emoji) + await self._state.http.add_reaction(self.channel.id, self.id, emoji, type=1 if boost else 0) - async def remove_reaction(self, emoji: Union[EmojiInputType, Reaction], member: Snowflake) -> None: + async def remove_reaction(self, emoji: Union[EmojiInputType, Reaction], member: Snowflake, boost: bool = False) -> None: """|coro| Remove a reaction by the member from the message. @@ -1261,6 +1284,14 @@ class PartialMessage(Hashable): The emoji to remove. member: :class:`abc.Snowflake` The member for which to remove the reaction. + boost: :class:`bool` + Whether to remove a super reaction. + + .. note:: + + Keep in mind that members can both react and super react with the same emoji. + + .. versionadded:: 2.1 Raises -------- @@ -1276,9 +1307,9 @@ class PartialMessage(Hashable): emoji = convert_emoji_reaction(emoji) if member.id == self._state.self_id: - await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji) + await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji, type=1 if boost else 0) else: - await self._state.http.remove_reaction(self.channel.id, self.id, emoji, member.id) + await self._state.http.remove_reaction(self.channel.id, self.id, emoji, member.id, type=1 if boost else 0) async def clear_reaction(self, emoji: Union[EmojiInputType, Reaction]) -> None: """|coro| @@ -1657,7 +1688,7 @@ class PartialMessage(Hashable): .. versionadded:: 1.7 type: :class:`MessageReferenceType` - The type of message reference. + The type of message reference. Default :attr:`MessageReferenceType.reply`. .. versionadded:: 2.1 @@ -1967,7 +1998,6 @@ class Message(PartialMessage, Hashable): self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] self.call: Optional[CallMessage] = None self.interaction: Optional[Interaction] = None - self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots')) self.poll: Optional[Poll] = None try: @@ -2036,6 +2066,8 @@ class Message(PartialMessage, Hashable): # The channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'), self.reference) # type: ignore + self.role_subscription: Optional[RoleSubscriptionInfo] = None try: role_subscription = data['role_subscription_data'] diff --git a/discord/permissions.py b/discord/permissions.py index 533f7aed3..3b7349d1c 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -257,10 +257,7 @@ class Permissions(BaseFlags): no longer part of the general permissions. .. versionchanged:: 2.1 - Added :attr:`create_expressions` permission. - - .. versionchanged:: 2.1 - Added :attr:`view_creator_monetization_analytics` permission. + Added :attr:`create_expressions` and :attr:`view_creator_monetization_analytics` permission. """ return cls(0b0000_0000_0000_0000_0000_1010_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) @@ -290,10 +287,7 @@ class Permissions(BaseFlags): :attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions. .. versionchanged:: 2.1 - Added :attr:`send_voice_messages` permission. - - .. versionchanged:: 2.1 - Added :attr:`send_polls` and :attr:`use_external_apps` permissions. + Added :attr:`send_voice_messages`, :attr:`send_polls`, and :attr:`use_external_apps` permissions. """ return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) @@ -893,6 +887,7 @@ class PermissionOverwrite: send_polls: Optional[bool] create_polls: Optional[bool] use_external_apps: Optional[bool] + view_creator_monetization_analytics: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/player.py b/discord/player.py index 34433ba31..8bb927ebe 100644 --- a/discord/player.py +++ b/discord/player.py @@ -199,7 +199,7 @@ class FFmpegAudio(AudioSource): self._pipe_reader_thread.start() def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen: - _log.debug('Spawning ffmpeg process with command: %s', args) + _log.debug('Spawning ffmpeg process with command: %s.', args) process = None try: process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs) diff --git a/discord/raw_models.py b/discord/raw_models.py index 96d64fd28..6cd73e3f7 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -31,6 +31,8 @@ from .enums import ChannelType, ReactionType, ReadStateType, try_enum from .utils import _get_as_snowflake if TYPE_CHECKING: + from typing_extensions import Self + from .guild import Guild from .member import Member from .message import Message diff --git a/discord/reaction.py b/discord/reaction.py index 796e5c731..3eb856d26 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -77,11 +77,11 @@ class Reaction: count: :class:`int` Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`. me: :class:`bool` - If the user sent this reaction. + If the user has reacted with this emoji. message: :class:`Message` Message this reaction is for. me_burst: :class:`bool` - If the user sent this super reaction. + If the user has super-reacted with this emoji. .. versionadded:: 2.1 normal_count: :class:`int` @@ -105,11 +105,12 @@ class Reaction: self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data['emoji']) self.count: int = data.get('count', 1) self.me: bool = data['me'] - details = data.get('count_details', {}) - self.normal_count: int = details.get('normal', 0) - self.burst_count: int = details.get('burst', 0) self.me_burst: bool = data.get('me_burst', False) + details = data.get('count_details', {}) + self.normal_count: int = details.get('normal', int(self.me)) + self.burst_count: int = details.get('burst', int(self.me_burst)) + # TODO: typeguard def is_custom_emoji(self) -> bool: """:class:`bool`: If this is a custom emoji.""" @@ -132,7 +133,7 @@ class Reaction: def __repr__(self) -> str: return f'' - async def remove(self, user: Snowflake) -> None: + async def remove(self, user: Snowflake, *, boost: bool = False) -> None: """|coro| Remove the reaction by the provided :class:`User` from the message. @@ -147,6 +148,14 @@ class Reaction: ----------- user: :class:`abc.Snowflake` The user or member from which to remove the reaction. + boost: :class:`bool` + Whether to remove a super reaction. + + .. note:: + + Keep in mind that members can both react and super react with the same emoji. + + .. versionadded:: 2.1 Raises ------- @@ -158,7 +167,7 @@ class Reaction: The user you specified, or the reaction's message was not found. """ - await self.message.remove_reaction(self.emoji, user) + await self.message.remove_reaction(self.emoji, user, boost=boost) async def clear(self) -> None: """|coro| diff --git a/discord/state.py b/discord/state.py index b69dcfd1f..e0186b5d5 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1016,6 +1016,7 @@ class ConnectionState: self._status: Optional[str] = status self._afk: bool = options.get('afk', False) self._idle_since: int = since + self.overriden_rtc_regions: Optional[List[str]] = options.get('preferred_rtc_regions', None) if cache_flags._empty: self.store_user = self.create_user @@ -1131,10 +1132,6 @@ class ConnectionState: def locale(self) -> str: return str(getattr(self.user, 'locale', 'en-US')) - @property - def preferred_rtc_region(self) -> str: - return self.preferred_rtc_regions[0] if self.preferred_rtc_regions else 'us-central' - @property def voice_clients(self) -> List[VoiceProtocol]: return list(self._voice_clients.values()) @@ -1176,6 +1173,12 @@ class ConnectionState: def _remove_voice_client(self, guild_id: int) -> None: self._voice_clients.pop(guild_id, None) + def _get_preferred_regions(self) -> Dict[str, Union[List[str], str]]: + regions = self.overriden_rtc_regions if self.overriden_rtc_regions is not None else self.client.preferred_rtc_regions + if regions: + return {'preferred_regions': regions, 'preferred_region': regions[0]} + return {} + def _update_references(self, ws: DiscordWebSocket) -> None: for vc in self.voice_clients: vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime). diff --git a/discord/types/channel.py b/discord/types/channel.py index 456f9edac..7e040de32 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -170,7 +170,15 @@ class MediaChannel(_BaseForumChannel): GuildChannel = Union[ - TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, DirectoryChannel, ThreadChannel, ForumChannel, MediaChannel + TextChannel, + NewsChannel, + VoiceChannel, + CategoryChannel, + StageChannel, + DirectoryChannel, + ThreadChannel, + ForumChannel, + MediaChannel, ] diff --git a/discord/types/guild.py b/discord/types/guild.py index 1e49aaf1a..44e92ffff 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -130,9 +130,11 @@ class Guild(UnavailableGuild, _GuildMedia): max_members: NotRequired[int] premium_subscription_count: NotRequired[int] max_video_channel_users: NotRequired[int] + max_stage_video_channel_users: NotRequired[int] # application_command_counts: ApplicationCommandCounts hub_type: Optional[Literal[0, 1, 2]] incidents_data: Optional[IncidentData] + safety_alerts_channel_id: Optional[Snowflake] class UserGuild(BaseGuild): @@ -201,3 +203,8 @@ class SupplementalGuild(UnavailableGuild): class BulkBanUserResponse(TypedDict): banned_users: Optional[List[Snowflake]] failed_users: Optional[List[Snowflake]] + + +class RTCRegion(TypedDict): + region: str + ips: List[str] diff --git a/discord/types/message.py b/discord/types/message.py index d8a0ca4ef..a1300118a 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -180,8 +180,8 @@ class MessageSnapshot(TypedDict): flags: NotRequired[int] mentions: List[UserWithMember] mention_roles: SnowflakeList - stickers_items: NotRequired[List[StickerItem]] - components: NotRequired[List[Component]] + sticker_items: NotRequired[List[StickerItem]] + components: NotRequired[List[MessageActionRow]] class Message(PartialMessage): diff --git a/discord/types/poll.py b/discord/types/poll.py index fabdbd48f..eeea1bed1 100644 --- a/discord/types/poll.py +++ b/discord/types/poll.py @@ -35,7 +35,7 @@ if TYPE_CHECKING: from .emoji import PartialEmoji -LayoutType = Literal[1] # 1 = Default +LayoutType = Literal[1, 2] # 1 = Default class PollMedia(TypedDict): diff --git a/discord/types/voice.py b/discord/types/voice.py index ba84e0642..864be9a20 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -76,7 +76,7 @@ class VoiceRegion(TypedDict): class VoiceServerUpdate(TypedDict): token: str - guild_id: Snowflake + guild_id: NotRequired[Snowflake] channel_id: Snowflake endpoint: Optional[str] diff --git a/discord/voice_client.py b/discord/voice_client.py index 70c3f6e78..2b556dc53 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -47,6 +47,7 @@ if TYPE_CHECKING: from .types.gateway import VoiceStateUpdateEvent as VoiceStateUpdatePayload from .types.voice import ( + GuildVoiceState as GuildVoiceStatePayload, VoiceServerUpdate as VoiceServerUpdatePayload, TransportEncryptionModes, ) @@ -128,7 +129,9 @@ class VoiceProtocol: """ raise NotImplementedError - async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False) -> None: + async def connect( + self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False + ) -> None: """|coro| An abstract method called when the client initiates the connection request. @@ -302,9 +305,16 @@ class VoiceClient(VoiceProtocol): async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None: await self._connection.voice_server_update(data) - async def connect(self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False) -> None: + async def connect( + self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False + ) -> None: await self._connection.connect( - reconnect=reconnect, timeout=timeout, self_deaf=self_deaf, self_mute=self_mute, resume=False + reconnect=reconnect, + timeout=timeout, + self_deaf=self_deaf, + self_mute=self_mute, + self_video=self_video, + resume=False, ) def wait_until_connected(self, timeout: Optional[float] = 30.0) -> bool: @@ -541,10 +551,10 @@ class VoiceClient(VoiceProtocol): @source.setter def source(self, value: AudioSource) -> None: if not isinstance(value, AudioSource): - raise TypeError(f'expected AudioSource not {value.__class__.__name__}.') + raise TypeError(f'expected AudioSource not {value.__class__.__name__}') if self._player is None: - raise ValueError('Not playing anything.') + raise ValueError('Not playing anything') self._player.set_source(value) @@ -567,7 +577,6 @@ class VoiceClient(VoiceProtocol): opus.OpusError Encoding the data failed. """ - self.checked_add('sequence', 1, 65535) if encode: encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME) @@ -577,6 +586,6 @@ class VoiceClient(VoiceProtocol): try: self._connection.send_packet(packet) except OSError: - _log.debug('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) + _log.debug('A packet has been dropped (seq: %s, timestamp: %s).', self.sequence, self.timestamp) self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295) diff --git a/discord/voice_state.py b/discord/voice_state.py index fba07f5e8..4a25d81d4 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -148,7 +148,7 @@ class SocketReader(threading.Thread): readable, _, _ = select.select([self.state.socket], [], [], 30) except (ValueError, TypeError, OSError) as e: _log.debug( - "Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e + 'Select error handling socket in reader, this should be safe to ignore: %s: %s.', e.__class__.__name__, e ) # The socket is either closed or doesn't exist at the moment continue @@ -159,13 +159,13 @@ class SocketReader(threading.Thread): try: data = self.state.socket.recv(2048) except OSError: - _log.debug('Error reading from socket in %s, this should be safe to ignore', self, exc_info=True) + _log.debug('Error reading from socket in %s, this should be safe to ignore.', self, exc_info=True) else: for cb in self._callbacks: try: cb(data) except Exception: - _log.exception('Error calling %s in %s', cb, self) + _log.exception('Error calling %s in %s.', cb, self) class ConnectionFlowState(Enum): @@ -195,6 +195,7 @@ class VoiceConnectionState: self.reconnect: bool = True self.self_deaf: bool = False self.self_mute: bool = False + self.self_video: bool = False self.token: Optional[str] = None self.session_id: Optional[str] = None self.endpoint: Optional[str] = None @@ -226,7 +227,7 @@ class VoiceConnectionState: @state.setter def state(self, state: ConnectionFlowState) -> None: if state is not self._state: - _log.debug('Connection state changed to %s', state.name) + _log.debug('Voice connection state changed to %s.', state.name) self._state = state self._state_event.set() self._state_event.clear() @@ -298,11 +299,12 @@ class VoiceConnectionState: timeout=self.timeout, self_deaf=(self.self_voice_state or self).self_deaf, self_mute=(self.self_voice_state or self).self_mute, + self_video=(self.self_voice_state or self).self_video, resume=False, wait=False, ) else: - _log.debug('Ignoring unexpected voice_state_update event') + _log.debug('Ignoring unexpected VOICE_STATE_UPDATE event.') async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: previous_token = self.token @@ -310,13 +312,13 @@ class VoiceConnectionState: previous_endpoint = self.endpoint self.token = data['token'] - self.server_id = int(data['guild_id']) + self.server_id = int(data.get('guild_id', data['channel_id'])) endpoint = data.get('endpoint') if self.token is None or endpoint is None: _log.warning( 'Awaiting endpoint... This requires waiting. ' - 'If timeout occurred considering raising the timeout and reconnecting.' + 'If timeout occurrs, considering raising the timeout and reconnecting.' ) return @@ -337,15 +339,15 @@ class VoiceConnectionState: self.state = ConnectionFlowState.got_both_voice_updates elif self.state is ConnectionFlowState.connected: - _log.debug('Voice server update, closing old voice websocket') + _log.debug('Got VOICE_SERVER_UPDATE, closing old voice gateway.') await self.ws.close(4014) self.state = ConnectionFlowState.got_voice_server_update elif self.state is not ConnectionFlowState.disconnected: # eventual consistency - if previous_token == self.token and previous_server_id == self.server_id: + if previous_token == self.token and previous_server_id == self.server_id and previous_endpoint == self.endpoint: return - _log.debug('Unexpected server update event, attempting to handle') + _log.debug('Unexpected VOICE_SERVER_UPDATE event, attempting to handle...') await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update) await self.connect( @@ -353,13 +355,22 @@ class VoiceConnectionState: timeout=self.timeout, self_deaf=(self.self_voice_state or self).self_deaf, self_mute=(self.self_voice_state or self).self_mute, + self_video=(self.self_voice_state or self).self_video, resume=False, wait=False, ) self._create_socket() async def connect( - self, *, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool, wait: bool = True + self, + *, + reconnect: bool, + timeout: float, + self_deaf: bool, + self_mute: bool, + self_video: bool, + resume: bool, + wait: bool = True, ) -> None: if self._connector: self._connector.cancel() @@ -372,7 +383,7 @@ class VoiceConnectionState: self.timeout = timeout self.reconnect = reconnect self._connector = self.voice_client.loop.create_task( - self._wrap_connect(reconnect, timeout, self_deaf, self_mute, resume), name='Voice connector' + self._wrap_connect(reconnect, timeout, self_deaf, self_mute, self_video, resume), name='Voice connector' ) if wait: await self._connector @@ -381,30 +392,32 @@ class VoiceConnectionState: try: await self._connect(*args) except asyncio.CancelledError: - _log.debug('Cancelling voice connection') + _log.debug('Cancelling voice connection.') await self.soft_disconnect() raise except asyncio.TimeoutError: - _log.info('Timed out connecting to voice') + _log.info('Timed out connecting to voice.') await self.disconnect() raise except Exception: - _log.exception('Error connecting to voice... disconnecting') + _log.exception('Error connecting to voice. Disconnecting.') await self.disconnect() raise - async def _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: + async def _inner_connect( + self, reconnect: bool, self_deaf: bool, self_mute: bool, self_video: bool, resume: bool + ) -> None: for i in range(5): - _log.info('Starting voice handshake... (connection attempt %d)', i + 1) + _log.info('Starting voice handshake (connection attempt %d)...', i + 1) - await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) + await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute, self_video=self_video) # Setting this unnecessarily will break reconnecting if self.state is ConnectionFlowState.disconnected: self.state = ConnectionFlowState.set_guild_voice_state await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) - _log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) + _log.info('Voice handshake complete. Endpoint found: %s.', self.endpoint) try: self.ws = await self._connect_websocket(resume) @@ -421,11 +434,15 @@ class VoiceConnectionState: await self.disconnect() raise - async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: + async def _connect( + self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, self_video: bool, resume: bool + ) -> None: _log.info('Connecting to voice...') await asyncio.wait_for( - self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), + self._inner_connect( + reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video, resume=resume + ), timeout=timeout, ) _log.info('Voice connection complete.') @@ -442,7 +459,7 @@ class VoiceConnectionState: if self.ws: await self.ws.close() except Exception: - _log.debug('Ignoring exception disconnecting from voice', exc_info=True) + _log.debug('Ignoring exception disconnecting from voice.', exc_info=True) finally: self.state = ConnectionFlowState.disconnected self._socket_reader.pause() @@ -471,7 +488,7 @@ class VoiceConnectionState: try: await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) except TimeoutError: - _log.debug('Timed out waiting for voice disconnection confirmation') + _log.debug('Timed out waiting for voice disconnection confirmation.') except asyncio.CancelledError: pass @@ -489,7 +506,7 @@ class VoiceConnectionState: if self.ws: await self.ws.close() except Exception: - _log.debug('Ignoring exception soft disconnecting from voice', exc_info=True) + _log.debug('Ignoring exception soft disconnecting from voice.', exc_info=True) finally: self.state = with_state self._socket_reader.pause() @@ -518,9 +535,13 @@ class VoiceConnectionState: try: await self.wait_async(timeout) except asyncio.TimeoutError: - _log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id if self.guild else 'private') + _log.warning( + 'Timed out trying to move to channel %s in guild %s.', + channel.id, + self.guild.id if self.guild else '"private"', + ) if self.state is last_state: - _log.debug('Reverting to previous state %s', previous_state.name) + _log.debug('Reverting to previous voice state %s.', previous_state.name) self.state = previous_state def wait(self, timeout: Optional[float] = None) -> bool: @@ -536,11 +557,11 @@ class VoiceConnectionState: self.socket.sendall(packet) def add_socket_listener(self, callback: SocketReaderCallback) -> None: - _log.debug('Registering socket listener callback %s', callback) + _log.debug('Registering voice socket listener callback %s.', callback) self._socket_reader.register(callback) def remove_socket_listener(self, callback: SocketReaderCallback) -> None: - _log.debug('Unregistering socket listener callback %s', callback) + _log.debug('Unregistering voice socket listener callback %s.', callback) self._socket_reader.unregister(callback) def _inside_runner(self) -> bool: @@ -555,18 +576,22 @@ class VoiceConnectionState: return await sane_wait_for([self._state_event.wait()], timeout=timeout) - async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False) -> None: + async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False) -> None: channel = self.voice_client.channel if self.guild: - await self.guild.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) + await self.guild.change_voice_state( + channel=channel, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video + ) else: - await self.voice_client._state.client.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) + await self.voice_client._state.client.change_voice_state( + channel=channel, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video + ) async def _voice_disconnect(self) -> None: _log.info( - 'The voice handshake is being terminated for Channel ID %s (Guild ID %s)', + 'The voice handshake is being terminated for channel ID %s (guild ID %s).', self.voice_client.channel.id, - self.guild.id if self.guild else 'private', + self.guild.id if self.guild else '"private"', ) self.state = ConnectionFlowState.disconnected if self.guild: @@ -618,12 +643,12 @@ class VoiceConnectionState: # We were disconnected by discord # This condition is a race between the main ws event and the voice ws closing if self._disconnected.is_set(): - _log.info('Disconnected from voice by discord, close code %d.', exc.code) + _log.info('Disconnected from voice by Discord, close code %d.', exc.code) await self.disconnect() break # We may have been moved to a different channel - _log.info('Disconnected from voice by force... potentially reconnecting.') + _log.info('Disconnected from voice by force. Potentially reconnecting...') successful = await self._potential_reconnect() if not successful: _log.info('Reconnect was unsuccessful, disconnecting from voice normally...') @@ -634,7 +659,7 @@ class VoiceConnectionState: else: continue - _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') + _log.debug('Not handling close code %s (%s).', exc.code, exc.reason or 'no reason') if not reconnect: await self.disconnect() @@ -651,6 +676,7 @@ class VoiceConnectionState: timeout=self.timeout, self_deaf=(self.self_voice_state or self).self_deaf, self_mute=(self.self_voice_state or self).self_mute, + self_video=(self.self_voice_state or self).self_video, resume=False, ) except asyncio.TimeoutError: diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 23c2416eb..ce4873fbf 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -58,6 +58,7 @@ __all__ = ( _log = logging.getLogger(__name__) if TYPE_CHECKING: + from datetime import datetime from typing_extensions import Self from types import TracebackType @@ -807,7 +808,7 @@ class BaseWebhook(Hashable): return guild and guild.get_channel(self.channel_id) # type: ignore @property - def created_at(self) -> datetime.datetime: + def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the webhook's creation time in UTC.""" return utils.snowflake_time(self.id) diff --git a/docs/api.rst b/docs/api.rst index 92fe8e1db..fcdec3c30 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1977,22 +1977,6 @@ of :class:`enum.Enum`. .. versionadded:: 2.0 -.. class:: InviteType - - Specifies the type of :class:`Invite`. - - .. attribute:: guild - - A guild invite. - - .. attribute:: group_dm - - A group DM invite. - - .. attribute:: friend - - A friend invite. - .. attribute:: guild_incident_alert_mode_enabled The system message sent when security actions is enabled. @@ -2023,6 +2007,22 @@ of :class:`enum.Enum`. .. versionadded:: 2.1 +.. class:: InviteType + + Specifies the type of :class:`Invite`. + + .. attribute:: guild + + A guild invite. + + .. attribute:: group_dm + + A group DM invite. + + .. attribute:: friend + + A friend invite. + .. class:: UserFlags Represents Discord User flags. @@ -8261,14 +8261,6 @@ DirectoryEntry .. autoclass:: DirectoryEntry() :members: -RoleFlags -~~~~~~~~~~ - -.. attributetable:: RoleFlags - -.. autoclass:: RoleFlags - :members: - ForumTag ~~~~~~~~~ @@ -8429,6 +8421,11 @@ Flags .. autoclass:: ReadStateFlags() :members: +.. attributetable:: RoleFlags + +.. autoclass:: RoleFlags() + :members: + .. attributetable:: SKUFlags .. autoclass:: SKUFlags()