diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a0a537b09..218dd54ec 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,7 +35,7 @@ jobs: with: version: '1.1.289' warnings: false - no-comments: ${{ matrix.python-version != '3.x' }} + no-comments: false - name: Run black if: ${{ always() && steps.install-deps.outcome == 'success' }} diff --git a/discord/abc.py b/discord/abc.py index a5c7e5f08..d8b8dd873 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -90,6 +90,7 @@ if TYPE_CHECKING: VocalGuildChannel, VoiceChannel, StageChannel, + CategoryChannel, ) from .threads import Thread from .enums import InviteTarget diff --git a/discord/activity.py b/discord/activity.py index 70995e6c7..e22270423 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -836,42 +836,42 @@ class CustomActivity(BaseActivity): return ActivityType.custom def to_dict(self) -> ActivityPayload: - o = { + payload = { 'type': ActivityType.custom.value, 'state': self.name, 'name': 'Custom Status', # Not a confusing API at all } if self.emoji: - o['emoji'] = self.emoji.to_dict() - return o # type: ignore + payload['emoji'] = self.emoji.to_dict() + return payload # type: ignore - def to_legacy_settings_dict(self) -> Dict[str, Any]: - o: Dict[str, Optional[Union[str, int]]] = {} + def to_legacy_settings_dict(self) -> Dict[str, Optional[Union[str, int]]]: + payload: Dict[str, Optional[Union[str, int]]] = {} if self.name: - o['text'] = self.name + payload['text'] = self.name if self.emoji: emoji = self.emoji - o['emoji_name'] = emoji.name + payload['emoji_name'] = emoji.name if emoji.id: - o['emoji_id'] = emoji.id + payload['emoji_id'] = emoji.id if self.expires_at is not None: - o['expires_at'] = self.expires_at.isoformat() - return o + payload['expires_at'] = self.expires_at.isoformat() + return payload - def to_settings_dict(self) -> Dict[str, Any]: - o: Dict[str, Optional[Union[str, int]]] = {} + def to_settings_dict(self) -> Dict[str, Optional[Union[str, int]]]: + payload: Dict[str, Optional[Union[str, int]]] = {} if self.name: - o['text'] = self.name + payload['text'] = self.name if self.emoji: emoji = self.emoji - o['emoji_name'] = emoji.name + payload['emoji_name'] = emoji.name if emoji.id: - o['emoji_id'] = emoji.id + payload['emoji_id'] = emoji.id if self.expires_at is not None: - o['expires_at_ms'] = int(self.expires_at.timestamp() * 1000) - return o + payload['expires_at_ms'] = int(self.expires_at.timestamp() * 1000) + return payload def __eq__(self, other: object) -> bool: return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji diff --git a/discord/application.py b/discord/application.py index a08917302..1cf009d5d 100644 --- a/discord/application.py +++ b/discord/application.py @@ -772,8 +772,7 @@ class ApplicationInstallParams: application_id: :class:`int` The ID of the application to be authorized. scopes: List[:class:`str`] - The list of `OAuth2 scopes `_ - to add the application with. + The list of :ddocs:`OAuth2 scopes ` to add the application with. permissions: :class:`Permissions` The permissions to grant to the added bot. """ @@ -1652,7 +1651,7 @@ class PartialApplication(Hashable): A list of RPC origin URLs, if RPC is enabled. verify_key: :class:`str` The hex encoded key for verification in interactions and the - GameSDK's `GetTicket `_. + GameSDK's :ddocs:`GetTicket None: + def __init__(self, message: Message, *, participants: List[User], ended_timestamp: Optional[str]) -> None: self.message = message self.ended_timestamp = utils.parse_time(ended_timestamp) self.participants = participants @@ -188,8 +188,8 @@ class PrivateCall: return list(self._ringing) @property - def initiator(self) -> User: - """:class:`.abc.User`: Returns the user that started the call.""" + def initiator(self) -> Optional[User]: + """Optional[:class:`.abc.User`]: Returns the user that started the call. Returns ``None`` if the message is not cached.""" return getattr(self.message, 'author', None) @property @@ -211,10 +211,9 @@ class PrivateCall: } @cached_slot_property('_cs_message') - def message(self) -> Message: - """:class:`Message`: The message associated with this call.""" - # Lying to the type checker for better developer UX, very unlikely for the message to not be received - return self._state._get_message(self._message_id) # type: ignore + def message(self) -> Optional[Message]: + """Optional[:class:`Message`]: The message associated with this call. Sometimes may not be cached.""" + return self._state._get_message(self._message_id) async def fetch_message(self) -> Message: """|coro| diff --git a/discord/channel.py b/discord/channel.py index 70f7c14a6..06b3eb57f 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -59,6 +59,8 @@ from .threads import Thread from .partial_emoji import _EmojiTag, PartialEmoji from .flags import ChannelFlags from .http import handle_message_parameters +from .invite import Invite +from .voice_client import VoiceClient __all__ = ( 'TextChannel', @@ -80,7 +82,7 @@ if TYPE_CHECKING: from .role import Role from .object import Object from .member import Member, VoiceState - from .abc import Snowflake, SnowflakeTime + from .abc import Snowflake, SnowflakeTime, T from .message import Message, PartialMessage, EmojiInputType from .mentions import AllowedMentions from .webhook import Webhook @@ -1974,8 +1976,6 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): class ForumTag(Hashable): """Represents a forum tag that can be applied to a thread within a :class:`ForumChannel`. - .. versionadded:: 2.0 - .. container:: operations .. describe:: x == y @@ -1994,6 +1994,7 @@ class ForumTag(Hashable): Returns the forum tag's name. + .. versionadded:: 2.0 Attributes ----------- @@ -2602,11 +2603,9 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): channel_payload = { 'name': name, 'auto_archive_duration': auto_archive_duration or self.default_auto_archive_duration, - 'location': 'Forum Channel', + 'rate_limit_per_user': slowmode_delay, 'type': 11, # Private threads don't seem to be allowed } - if slowmode_delay is not None: - extras['rate_limit_per_user'] = slowmode_delay if applied_tags is not MISSING: channel_payload['applied_tags'] = [str(tag.id) for tag in applied_tags] @@ -2629,8 +2628,6 @@ class ForumChannel(discord.abc.GuildChannel, Hashable): data = await state.http.start_thread_in_forum(self.id, params=params, reason=reason) thread = Thread(guild=self.guild, state=self._state, data=data) message = Message(state=self._state, channel=thread, data=data['message']) - if view: - self._state.store_view(view, message.id) return ThreadWithMessage(thread=thread, message=message) @@ -3026,9 +3023,9 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr *, timeout: float = 60.0, reconnect: bool = True, - cls: Callable[[Client, discord.abc.Connectable], ConnectReturn] = MISSING, + cls: Callable[[Client, discord.abc.Connectable], T] = VoiceClient, ring: bool = True, - ) -> ConnectReturn: + ) -> T: """|coro| Connects to voice and creates a :class:`~discord.VoiceClient` to establish @@ -3558,9 +3555,9 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc *, timeout: float = 60.0, reconnect: bool = True, - cls: Callable[[Client, discord.abc.Connectable], ConnectReturn] = MISSING, + cls: Callable[[Client, discord.abc.Connectable], T] = VoiceClient, ring: bool = True, - ) -> ConnectReturn: + ) -> T: await self._get_channel() call = self.call if call is None and ring: diff --git a/discord/client.py b/discord/client.py index b65d263af..363a89bba 100644 --- a/discord/client.py +++ b/discord/client.py @@ -3690,7 +3690,7 @@ class Client: _state = self._connection - async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[Snowflake]): + async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]): after_id = after.id if after else None data = await _state.http.get_payments(retrieve, after=after_id) @@ -3702,7 +3702,7 @@ class Client: return data, after, limit - async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[Snowflake]): + async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): before_id = before.id if before else None data = await _state.http.get_payments(retrieve, before=before_id) @@ -4537,7 +4537,7 @@ class Client: """ _state = self._connection - async def strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[Snowflake]): + async def strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): before_id = before.id if before else None data = await _state.http.get_recent_mentions( retrieve, before=before_id, guild_id=guild.id if guild else None, roles=roles, everyone=everyone @@ -4552,14 +4552,16 @@ class Client: return data, before, limit if isinstance(before, datetime): - before = Object(id=utils.time_snowflake(before, high=False)) + state = Object(id=utils.time_snowflake(before, high=False)) + else: + state = before while True: retrieve = min(100 if limit is None else limit, 100) if retrieve < 1: return - data, before, limit = await strategy(retrieve, before, limit) + data, state, limit = await strategy(retrieve, state, limit) # Terminate loop on next iteration; there's no data left after this if len(data) < 100: diff --git a/discord/components.py b/discord/components.py index ffd355120..f8bca4cea 100644 --- a/discord/components.py +++ b/discord/components.py @@ -72,7 +72,7 @@ class Component: .. versionadded:: 2.0 """ - __slots__: Tuple[str, ...] = ('type', 'message') + __slots__ = ('message',) __repr_info__: ClassVar[Tuple[str, ...]] message: Message @@ -119,7 +119,7 @@ class ActionRow(Component): The originating message. """ - __slots__: Tuple[str, ...] = ('children',) + __slots__ = ('children',) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -165,7 +165,7 @@ class Button(Component): The originating message. """ - __slots__: Tuple[str, ...] = ( + __slots__ = ( 'style', 'custom_id', 'url', @@ -189,17 +189,17 @@ class Button(Component): except KeyError: self.emoji = None + @property + def type(self) -> Literal[ComponentType.button]: + """:class:`ComponentType`: The type of component.""" + return ComponentType.button + def to_dict(self) -> dict: return { 'component_type': self.type.value, 'custom_id': self.custom_id, } - @property - def type(self) -> Literal[ComponentType.button]: - """:class:`ComponentType`: The type of component.""" - return ComponentType.button - async def click(self) -> Union[str, Interaction]: """|coro| @@ -261,7 +261,7 @@ class SelectMenu(Component): The originating message, if any. """ - __slots__: Tuple[str, ...] = ( + __slots__ = ( 'custom_id', 'placeholder', 'min_values', @@ -350,7 +350,7 @@ class SelectOption: Whether this option is selected by default. """ - __slots__: Tuple[str, ...] = ( + __slots__ = ( 'label', 'value', 'description', @@ -437,7 +437,7 @@ class TextInput(Component): The maximum length of the text input. """ - __slots__: Tuple[str, ...] = ( + __slots__ = ( 'style', 'label', 'custom_id', @@ -466,13 +466,6 @@ class TextInput(Component): """:class:`ComponentType`: The type of component.""" return ComponentType.text_input - def to_dict(self) -> dict: - return { - 'type': self.type.value, - 'custom_id': self.custom_id, - 'value': self.value, - } - @property def value(self) -> Optional[str]: """Optional[:class:`str`]: The current value of the text input. Defaults to :attr:`default`. @@ -514,18 +507,31 @@ class TextInput(Component): """ self.value = value + def to_dict(self) -> dict: + return { + 'type': self.type.value, + 'custom_id': self.custom_id, + 'value': self.value, + } + @overload -def _component_factory(data: ActionRowChildComponentPayload, message: Message = ...) -> Optional[ActionRowChildComponentType]: +def _component_factory( + data: ActionRowChildComponentPayload, message: Message = ... +) -> Optional[ActionRowChildComponentType]: ... @overload -def _component_factory(data: ComponentPayload, message: Message = ...) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory( + data: ComponentPayload, message: Message = ... +) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: ... -def _component_factory(data: ComponentPayload, message: Message = MISSING) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory( + data: ComponentPayload, message: Message = MISSING +) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: if data['type'] == 1: return ActionRow(data, message) elif data['type'] == 2: diff --git a/discord/entitlements.py b/discord/entitlements.py index 4bea2fc1d..67c21e4cd 100644 --- a/discord/entitlements.py +++ b/discord/entitlements.py @@ -491,7 +491,7 @@ class Gift: payment_source: Optional[:class:`PaymentSource`] The payment source to use for the redemption. Only required if the gift's :attr:`flags` have :attr:`GiftFlags.payment_source_required` set to ``True``. - channel: Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]] + channel: Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]] The channel to redeem the gift in. This is usually the channel the gift was sent in. While this is optional, it is recommended to pass this in. gateway_checkout_context: Optional[:class:`str`] diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 3931c08c8..2525c9e95 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1064,8 +1064,6 @@ else: - ``Range[float, 1.0, 5.0]`` means the minimum is 1.0 and the maximum is 5.0. - ``Range[str, 1, 10]`` means the minimum length is 1 and the maximum length is 10. - Inside a :class:`HybridCommand` this functions equivalently to :class:`discord.app_commands.Range`. - If the value cannot be converted to the provided type or is outside the given range, :class:`~.ext.commands.BadArgument` or :class:`~.ext.commands.RangeError` is raised to the appropriate error handlers respectively. diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 57c3a0a26..7145e151b 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -119,7 +119,8 @@ class Cooldown: if not current: current = time.time() - tokens = self._tokens + # the calculated tokens should be non-negative + tokens = max(self._tokens, 0) if current > self._window + self.per: tokens = self.rate @@ -147,7 +148,7 @@ class Cooldown: return 0.0 - def update_rate_limit(self, current: Optional[float] = None) -> Optional[float]: + def update_rate_limit(self, current: Optional[float] = None, *, tokens: int = 1) -> Optional[float]: """Updates the cooldown rate limit. Parameters @@ -155,6 +156,10 @@ class Cooldown: current: Optional[:class:`float`] The time in seconds since Unix epoch to update the rate limit at. If not supplied, then :func:`time.time()` is used. + tokens: :class:`int` + The amount of tokens to deduct from the rate limit. + + .. versionadded:: 2.0 Returns ------- @@ -170,12 +175,12 @@ class Cooldown: if self._tokens == self.rate: self._window = current - # check if we are rate limited - if self._tokens == 0: - return self.per - (current - self._window) + # decrement tokens by specified number + self._tokens -= tokens - # we're not so decrement our tokens - self._tokens -= 1 + # check if we are rate limited and return retry-after + if self._tokens < 0: + return self.per - (current - self._window) def reset(self) -> None: """Reset the cooldown to its initial state.""" diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 4fecab401..b4b92e819 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -55,7 +55,6 @@ from .converter import Greedy, run_converters from .cooldowns import BucketType, Cooldown, CooldownMapping, DynamicCooldownMapping, MaxConcurrency from .errors import * from .parameters import Parameter, Signature -from discord.app_commands.commands import NUMPY_DOCSTRING_ARG_REGEX if TYPE_CHECKING: from typing_extensions import Concatenate, ParamSpec, Self @@ -92,6 +91,14 @@ __all__ = ( MISSING: Any = discord.utils.MISSING +ARG_NAME_SUBREGEX = r'(?:\\?\*){0,2}(?P\w+)' +ARG_DESCRIPTION_SUBREGEX = r'(?P(?:.|\n)+?(?:\Z|\r?\n(?=[\S\r\n])))' +ARG_TYPE_SUBREGEX = r'(?:.+)' +NUMPY_DOCSTRING_ARG_REGEX = re.compile( + rf'^{ARG_NAME_SUBREGEX}(?:[ \t]*:)?(?:[ \t]+{ARG_TYPE_SUBREGEX})?[ \t]*\r?\n[ \t]+{ARG_DESCRIPTION_SUBREGEX}', + re.MULTILINE, +) + T = TypeVar('T') CommandT = TypeVar('CommandT', bound='Command[Any, ..., Any]') # CHT = TypeVar('CHT', bound='Check') diff --git a/discord/flags.py b/discord/flags.py index 1d1af7c37..383cdd4b0 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -577,6 +577,15 @@ class MessageFlags(BaseFlags): """ return 256 + @flag_value + def link_not_discord_warning(self): + """:class:`bool`: Returns ``True`` if this message contains a link that impersonates + Discord and should show a warning. + + .. versionadded:: 2.0 + """ + return 1024 + @flag_value def suppress_notifications(self): """:class:`bool`: Returns ``True`` if the message will not trigger push and desktop notifications. @@ -593,6 +602,14 @@ class MessageFlags(BaseFlags): """ return 4096 + @flag_value + def is_voice_message(self): + """:class:`bool`: Returns ``True`` if the message's audio attachments are rendered as voice messages. + + .. versionadded:: 2.0 + """ + return 8192 + @fill_with_flags() class PublicUserFlags(BaseFlags): @@ -2223,6 +2240,7 @@ class AutoModPresets(ArrayFlags): r"""Wraps up the Discord :class:`AutoModRule` presets. .. container:: operations + .. describe:: x == y Checks if two AutoModPresets flags are equal. diff --git a/discord/gateway.py b/discord/gateway.py index 3de52a0ce..43159179e 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -580,6 +580,7 @@ class DiscordWebSocket: self.session_id = data['session_id'] self.gateway = yarl.URL(data['resume_gateway_url']) _log.info('Connected to Gateway: %s (Session ID: %s).', ', '.join(trace), self.session_id) + await self.voice_state() # Initial OP 4 elif event == 'RESUMED': self._trace = trace = data.get('_trace', []) diff --git a/discord/guild.py b/discord/guild.py index 1ccf9a363..7eb36e84d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -690,6 +690,8 @@ class Guild(Hashable): """List[:class:`ForumChannel`]: A list of forum channels that belongs to this guild. This is sorted by the position and are in UI order from top to bottom. + + .. versionadded:: 2.0 """ r = [ch for ch in self._channels.values() if isinstance(ch, ForumChannel)] r.sort(key=lambda c: (c.position, c.id)) @@ -2946,11 +2948,11 @@ class Guild(Hashable): self, *, name: str, - start_time: datetime.datetime, + start_time: datetime, entity_type: Literal[EntityType.external] = ..., privacy_level: PrivacyLevel = ..., location: str = ..., - end_time: datetime.datetime = ..., + end_time: datetime = ..., description: str = ..., image: bytes = ..., reason: Optional[str] = ..., @@ -2962,11 +2964,11 @@ class Guild(Hashable): self, *, name: str, - start_time: datetime.datetime, + start_time: datetime, entity_type: Literal[EntityType.stage_instance, EntityType.voice] = ..., privacy_level: PrivacyLevel = ..., channel: Snowflake = ..., - end_time: datetime.datetime = ..., + end_time: datetime = ..., description: str = ..., image: bytes = ..., reason: Optional[str] = ..., @@ -2978,10 +2980,10 @@ class Guild(Hashable): self, *, name: str, - start_time: datetime.datetime, + start_time: datetime, privacy_level: PrivacyLevel = ..., location: str = ..., - end_time: datetime.datetime = ..., + end_time: datetime = ..., description: str = ..., image: bytes = ..., reason: Optional[str] = ..., @@ -2993,10 +2995,10 @@ class Guild(Hashable): self, *, name: str, - start_time: datetime.datetime, + start_time: datetime, privacy_level: PrivacyLevel = ..., channel: Union[VoiceChannel, StageChannel] = ..., - end_time: datetime.datetime = ..., + end_time: datetime = ..., description: str = ..., image: bytes = ..., reason: Optional[str] = ..., @@ -3008,7 +3010,7 @@ class Guild(Hashable): *, name: str, start_time: datetime, - entity_type: EntityType, + entity_type: EntityType = MISSING, privacy_level: PrivacyLevel = MISSING, channel: Optional[Snowflake] = MISSING, location: str = MISSING, @@ -3032,7 +3034,9 @@ class Guild(Hashable): description: :class:`str` The description of the scheduled event. channel: Optional[:class:`abc.Snowflake`] - The channel to send the scheduled event to. + The channel to send the scheduled event to. If the channel is + a :class:`StageInstance` or :class:`VoiceChannel` then + it automatically sets the entity type. Required if ``entity_type`` is either :attr:`EntityType.voice` or :attr:`EntityType.stage_instance`. @@ -3047,7 +3051,11 @@ class Guild(Hashable): privacy_level: :class:`PrivacyLevel` The privacy level of the scheduled event. entity_type: :class:`EntityType` - The entity type of the scheduled event. + The entity type of the scheduled event. If the channel is a + :class:`StageInstance` or :class:`VoiceChannel` then this is + automatically set to the appropriate entity type. If no channel + is passed then the entity type is assumed to be + :attr:`EntityType.external` image: :class:`bytes` The image of the scheduled event. location: :class:`str` diff --git a/discord/http.py b/discord/http.py index e7572c793..8a9e03272 100644 --- a/discord/http.py +++ b/discord/http.py @@ -125,7 +125,6 @@ INTERNAL_API_VERSION = 9 _log = logging.getLogger(__name__) - async def json_or_text(response: aiohttp.ClientResponse) -> Union[Dict[str, Any], str]: text = await response.text(encoding='utf-8') try: @@ -2034,8 +2033,12 @@ class HTTPClient: def get_welcome_screen(self, guild_id: Snowflake) -> Response[welcome_screen.WelcomeScreen]: return self.request(Route('GET', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id)) - def edit_welcome_screen(self, guild_id: Snowflake, payload: dict, reason: Optional[str] = None) -> Response[welcome_screen.WelcomeScreen]: - return self.request(Route('PATCH', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), json=payload, reason=reason) + def edit_welcome_screen( + self, guild_id: Snowflake, payload: dict, reason: Optional[str] = None + ) -> Response[welcome_screen.WelcomeScreen]: + return self.request( + Route('PATCH', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), json=payload, reason=reason + ) # Invite management diff --git a/discord/invite.py b/discord/invite.py index 702141fb5..a2e97df56 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -544,7 +544,7 @@ class Invite(Hashable): channel = (guild.get_channel(channel_id) or Object(id=channel_id)) if channel_id is not None else None else: guild = state._get_or_create_unavailable_guild(guild_id) if guild_id is not None else None - channel = Object(id=channel_id) + channel = Object(id=channel_id) if channel_id is not None else None return cls(state=state, data=data, guild=guild, channel=channel) # type: ignore diff --git a/discord/message.py b/discord/message.py index 153c7f0c8..25f439318 100644 --- a/discord/message.py +++ b/discord/message.py @@ -76,8 +76,9 @@ if TYPE_CHECKING: from .types.message import ( Message as MessagePayload, Attachment as AttachmentPayload, - MessageReference as MessageReferencePayload, BaseApplication as MessageApplicationPayload, + Call as CallPayload, + MessageReference as MessageReferencePayload, MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, ) @@ -1622,7 +1623,7 @@ class Message(PartialMessage, Hashable): if role is not None: self.role_mentions.append(role) - def _handle_call(self, call) -> None: + def _handle_call(self, call: Optional[CallPayload]) -> None: if call is None or self.type is not MessageType.call: self.call = None return @@ -1636,8 +1637,7 @@ class Message(PartialMessage, Hashable): if user is not None: participants.append(user) - call['participants'] = participants - self.call = CallMessage(message=self, **call) + self.call = CallMessage(message=self, ended_timestamp=call.get('ended_timestamp'), participants=participants) def _handle_components(self, data: List[ComponentPayload]) -> None: self.components = [] @@ -1894,16 +1894,16 @@ class Message(PartialMessage, Hashable): return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!' if self.type is MessageType.role_subscription_purchase and self.role_subscription is not None: - # TODO: figure out how the message looks like for is_renewal: true total_months = self.role_subscription.total_months_subscribed 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}!' + action = 'renewed' if self.role_subscription.is_renewal else 'subscribed' + return f'{self.author.name} {action} **{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}**.' + return f'{self.author.name} started **{self.content}**' if self.type is MessageType.stage_end: - return f'{self.author.name} ended **{self.content}**.' + return f'{self.author.name} ended **{self.content}**' if self.type is MessageType.stage_speaker: return f'{self.author.name} is now a speaker.' @@ -1912,7 +1912,10 @@ class Message(PartialMessage, Hashable): 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}**.' + return f'{self.author.name} changed the Stage topic: **{self.content}**' + + if self.type is MessageType.guild_application_premium_subscription: + return f'{self.author.name} upgraded {self.application.name if self.application else "a deleted application"} to premium for this server!' # Fallback for unknown message types return self.content diff --git a/discord/opus.py b/discord/opus.py index 7cbdd8386..e1f08f515 100644 --- a/discord/opus.py +++ b/discord/opus.py @@ -122,7 +122,7 @@ signal_ctl: SignalCtl = { def _err_lt(result: int, func: Callable, args: List) -> int: if result < OK: - _log.debug('error has happened in %s', func.__name__) + _log.debug('Error has happened in %s.', func.__name__) raise OpusError(result) return result @@ -130,7 +130,7 @@ def _err_lt(result: int, func: Callable, args: List) -> int: def _err_ne(result: T, func: Callable, args: List) -> T: ret = args[-1]._obj if ret.value != OK: - _log.debug('error has happened in %s', func.__name__) + _log.debug('Error has happened in %s.', func.__name__) raise OpusError(ret.value) return result diff --git a/discord/player.py b/discord/player.py index c214135ce..b0079ebcb 100644 --- a/discord/player.py +++ b/discord/player.py @@ -196,7 +196,7 @@ class FFmpegAudio(AudioSource): try: proc.kill() except Exception: - _log.exception('Ignoring error attempting to kill ffmpeg process %s', proc.pid) + _log.exception('Ignoring error attempting to kill ffmpeg process %s.', proc.pid) if proc.poll() is None: _log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid) @@ -216,7 +216,7 @@ class FFmpegAudio(AudioSource): if self._stdin is not None: self._stdin.write(data) except Exception: - _log.debug('Write error for %s, this is probably not a problem', self, exc_info=True) + _log.debug('Write error for %s, this is probably not a problem.', self, exc_info=True) # at this point the source data is either exhausted or the process is fubar self._process.terminate() return @@ -509,7 +509,7 @@ class FFmpegOpusAudio(FFmpegAudio): if isinstance(method, str): probefunc = getattr(cls, '_probe_codec_' + method, None) if probefunc is None: - raise AttributeError(f"Invalid probe method {method!r}") + raise AttributeError(f'Invalid probe method {method!r}.') if probefunc is cls._probe_codec_native: fallback = cls._probe_codec_fallback @@ -526,18 +526,18 @@ class FFmpegOpusAudio(FFmpegAudio): codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) except Exception: if not fallback: - _log.exception("Probe '%s' using '%s' failed", method, executable) + _log.exception("Probe '%s' using '%s' failed.", method, executable) return # type: ignore - _log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable) + _log.exception("Probe '%s' using '%s' failed, trying fallback.", method, executable) try: codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) except Exception: - _log.exception("Fallback probe using '%s' failed", executable) + _log.exception("Fallback probe using '%s' failed.", executable) else: - _log.debug("Fallback probe found codec=%s, bitrate=%s", codec, bitrate) + _log.debug('Fallback probe found codec=%s, bitrate=%s.', codec, bitrate) else: - _log.debug("Probe found codec=%s, bitrate=%s", codec, bitrate) + _log.debug('Probe found codec=%s, bitrate=%s.', codec, bitrate) finally: return codec, bitrate diff --git a/discord/profile.py b/discord/profile.py index 6fbfb16be..06d1cb9c6 100644 --- a/discord/profile.py +++ b/discord/profile.py @@ -34,7 +34,6 @@ from .enums import PremiumType, try_enum from .flags import ApplicationFlags from .member import Member from .mixins import Hashable -from .object import Object from .user import Note, User if TYPE_CHECKING: @@ -96,9 +95,9 @@ class Profile: state = self._state def get_guild(guild): - return state._get_guild(int(guild['id'])) or Object(id=int(guild['id'])) + return state._get_or_create_unavailable_guild(int(guild['id'])) - return list(map(get_guild, mutual_guilds)) # type: ignore # Lying for better developer UX + return list(map(get_guild, mutual_guilds)) def _parse_mutual_friends(self, mutual_friends) -> Optional[List[User]]: if mutual_friends is None: diff --git a/discord/scheduled_event.py b/discord/scheduled_event.py index 89cdddaba..8194789c3 100644 --- a/discord/scheduled_event.py +++ b/discord/scheduled_event.py @@ -407,7 +407,9 @@ class ScheduledEvent(Hashable): description: :class:`str` The description of the scheduled event. channel: Optional[:class:`~discord.abc.Snowflake`] - The channel to put the scheduled event in. + The channel to put the scheduled event in. If the channel is + a :class:`StageInstance` or :class:`VoiceChannel` then + it automatically sets the entity type. Required if the entity type is either :attr:`EntityType.voice` or :attr:`EntityType.stage_instance`. @@ -426,7 +428,9 @@ class ScheduledEvent(Hashable): privacy_level: :class:`PrivacyLevel` The privacy level of the scheduled event. entity_type: :class:`EntityType` - The new entity type. + The new entity type. If the channel is a :class:`StageInstance` + or :class:`VoiceChannel` then this is automatically set to the + appropriate entity type. status: :class:`EventStatus` The new status of the scheduled event. image: Optional[:class:`bytes`] @@ -467,7 +471,7 @@ class ScheduledEvent(Hashable): if start_time is not MISSING: if start_time.tzinfo is None: raise ValueError( - 'start_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time' + 'start_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' ) payload['scheduled_start_time'] = start_time.isoformat() @@ -476,7 +480,7 @@ class ScheduledEvent(Hashable): if privacy_level is not MISSING: if not isinstance(privacy_level, PrivacyLevel): - raise TypeError('privacy_level must be of type PrivacyLevel') + raise TypeError('privacy_level must be of type PrivacyLevel.') payload['privacy_level'] = privacy_level.value diff --git a/discord/settings.py b/discord/settings.py index 8afd7d7e3..1297a175d 100644 --- a/discord/settings.py +++ b/discord/settings.py @@ -28,7 +28,7 @@ import base64 from datetime import datetime, timezone import struct import logging -from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Sequence, Tuple, Type, Union, overload +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union, overload from google.protobuf.json_format import MessageToDict, ParseDict from discord_protos import PreloadedUserSettings # , FrecencyUserSettings @@ -125,8 +125,18 @@ class _ProtoSettings: new.settings.CopyFrom(self.settings) return new - def _get_guild(self, id: int, /) -> Union[Guild, Object]: + @overload + def _get_guild(self, id: int, /, *, always_guild: Literal[True] = ...) -> Guild: + ... + + @overload + def _get_guild(self, id: int, /, *, always_guild: Literal[False] = ...) -> Union[Guild, Object]: + ... + + def _get_guild(self, id: int, /, *, always_guild: bool = False) -> Union[Guild, Object]: id = int(id) + if always_guild: + return self._state._get_or_create_unavailable_guild(id) return self._state._get_guild(id) or Object(id=id) def to_dict(self, *, with_defaults: bool = False) -> Dict[str, Any]: @@ -310,8 +320,8 @@ class UserSettings(_ProtoSettings): return try_enum(SpoilerRenderOptions, self.settings.text_and_images.render_spoilers.value or 'ON_CLICK') @property - def collapsed_emoji_picker_sections(self) -> Tuple[Union[EmojiPickerSection, Guild, Object], ...]: - """Tuple[Union[:class:`EmojiPickerSection`, :class:`Guild`, :class:`Object`]]: A list of emoji picker sections (including guild IDs) that are collapsed.""" + def collapsed_emoji_picker_sections(self) -> Tuple[Union[EmojiPickerSection, Guild], ...]: + """Tuple[Union[:class:`EmojiPickerSection`, :class:`Guild`]]: A list of emoji picker sections (including guild IDs) that are collapsed.""" return tuple( self._get_guild(section) if section.isdigit() else try_enum(EmojiPickerSection, section) for section in self.settings.text_and_images.emoji_picker_collapsed_sections @@ -321,7 +331,7 @@ class UserSettings(_ProtoSettings): def collapsed_sticker_picker_sections(self) -> Tuple[Union[StickerPickerSection, Guild, Object], ...]: """Tuple[Union[:class:`StickerPickerSection`, :class:`Guild`, :class:`Object`]]: A list of sticker picker sections (including guild and sticker pack IDs) that are collapsed.""" return tuple( - self._get_guild(section) if section.isdigit() else try_enum(StickerPickerSection, section) + self._get_guild(section, always_guild=False) if section.isdigit() else try_enum(StickerPickerSection, section) for section in self.settings.text_and_images.sticker_picker_collapsed_sections ) @@ -497,8 +507,8 @@ class UserSettings(_ProtoSettings): ) @property - def restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that you will not receive DMs from.""" + def restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that you will not receive DMs from.""" return list(map(self._get_guild, self.settings.privacy.restricted_guild_ids)) @property @@ -545,8 +555,8 @@ class UserSettings(_ProtoSettings): return FriendDiscoveryFlags._from_value(self.settings.privacy.friend_discovery_flags.value) @property - def activity_restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that your current activity will not be shown in.""" + def activity_restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that your current activity will not be shown in.""" return list(map(self._get_guild, self.settings.privacy.activity_restricted_guild_ids)) @property @@ -555,13 +565,13 @@ class UserSettings(_ProtoSettings): return self.settings.privacy.default_guilds_activity_restricted @property - def activity_joining_restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that will not be able to join your current activity.""" + def activity_joining_restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that will not be able to join your current activity.""" return list(map(self._get_guild, self.settings.privacy.activity_joining_restricted_guild_ids)) @property - def message_request_restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds whose originating DMs will not be filtered into your message requests.""" + def message_request_restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds whose originating DMs will not be filtered into your message requests.""" return list(map(self._get_guild, self.settings.privacy.message_request_restricted_guild_ids)) @property @@ -683,8 +693,8 @@ class UserSettings(_ProtoSettings): return [GuildFolder._from_settings(data=folder, state=state) for folder in self.settings.guild_folders.folders] @property - def guild_positions(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI.""" + def guild_positions(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI.""" return list(map(self._get_guild, self.settings.guild_folders.guild_positions)) # Favorites Settings @@ -1208,8 +1218,10 @@ class GuildFolder: return self def _get_guild(self, id, /) -> Union[Guild, Object]: + from .guild import Guild # circular import + id = int(id) - return self._state._get_guild(id) or Object(id=id) if self._state else Object(id=id) + return self._state._get_or_create_unavailable_guild(id) if self._state else Object(id=id, type=Guild) def to_dict(self) -> dict: ret = {} @@ -1460,7 +1472,7 @@ class GuildProgress: @property def guild(self) -> Optional[Guild]: """Optional[:class:`Guild`]: The guild this progress belongs to. ``None`` if state is not attached.""" - return self._state._get_guild(self.guild_id) if self._state is not None else None + return self._state._get_or_create_unavailable_guild(self.guild_id) if self._state is not None else None @property def hub_progress(self) -> HubProgressFlags: @@ -1682,9 +1694,8 @@ class LegacyUserSettings: def __repr__(self) -> str: return '' - def _get_guild(self, id: int, /) -> Union[Guild, Object]: - id = int(id) - return self._state._get_guild(id) or Object(id=id) + def _get_guild(self, id: int, /) -> Guild: + return self._state._get_or_create_unavailable_guild(int(id)) def _update(self, data: Dict[str, Any]) -> None: RAW_VALUES = { @@ -1824,16 +1835,16 @@ class LegacyUserSettings: return await self._state.client.edit_legacy_settings(**kwargs) @property - def activity_restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that your current activity will not be shown in. + def activity_restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that your current activity will not be shown in. .. versionadded:: 2.0 """ return list(map(self._get_guild, getattr(self, '_activity_restricted_guild_ids', []))) @property - def activity_joining_restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that will not be able to join your current activity. + def activity_joining_restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that will not be able to join your current activity. .. versionadded:: 2.0 """ @@ -1873,8 +1884,8 @@ class LegacyUserSettings: ] @property - def guild_positions(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI.""" + def guild_positions(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI.""" return list(map(self._get_guild, getattr(self, '_guild_positions', []))) @property @@ -1893,8 +1904,8 @@ class LegacyUserSettings: return getattr(self, '_passwordless', False) @property - def restricted_guilds(self) -> List[Union[Guild, Object]]: - """List[Union[:class:`Guild`, :class:`Object`]]: A list of guilds that you will not receive DMs from.""" + def restricted_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: A list of guilds that you will not receive DMs from.""" return list(map(self._get_guild, getattr(self, '_restricted_guilds', []))) @property @@ -2007,7 +2018,7 @@ class ChannelSettings: @property def channel(self) -> Union[GuildChannel, PrivateChannel]: """Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`]: Returns the channel these settings are for.""" - guild = self._state._get_guild(self._guild_id) + guild = self._state._get_or_create_unavailable_guild(self._guild_id) if self._guild_id else None if guild: channel = guild.get_channel(self._channel_id) else: @@ -2165,7 +2176,7 @@ class GuildSettings: If the returned value is a :class:`ClientUser` then the settings are for the user's private channels. """ if self._guild_id: - return self._state._get_guild(self._guild_id) or Object(id=self._guild_id) # type: ignore # Lying for better developer UX + return self._state._get_or_create_unavailable_guild(self._guild_id) return self._state.user # type: ignore # Should always be present here @property diff --git a/discord/state.py b/discord/state.py index e54417cf4..05a35d9a6 100644 --- a/discord/state.py +++ b/discord/state.py @@ -2631,7 +2631,7 @@ class ConnectionState: # parse_guild_application_commands_update = parse_nothing # Grabbed directly in command iterators def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: - if isinstance(channel, (TextChannel, Thread, VoiceChannel)): + if isinstance(channel, (TextChannel, Thread, VoiceChannel, StageChannel)): return channel.guild.get_member(user_id) return self.get_user(user_id) diff --git a/discord/team.py b/discord/team.py index 81acfd240..428d5d9e2 100644 --- a/discord/team.py +++ b/discord/team.py @@ -412,7 +412,7 @@ class Team(Hashable): The payout received. """ - async def strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[Snowflake]): + async def strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): before_id = before.id if before else None data = await self._state.http.get_team_payouts(self.id, limit=retrieve, before=before_id) diff --git a/discord/threads.py b/discord/threads.py index a27974cde..77907b943 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -261,10 +261,7 @@ class Thread(Messageable, Hashable): @property def jump_url(self) -> str: - """:class:`str`: Returns a URL that allows the client to jump to the thread. - - .. versionadded:: 2.0 - """ + """:class:`str`: Returns a URL that allows the client to jump to the thread.""" return f'https://discord.com/channels/{self.guild.id}/{self.id}' @property @@ -277,17 +274,14 @@ class Thread(Messageable, Hashable): @property def applied_tags(self) -> List[ForumTag]: - """List[:class:`ForumTag`]: A list of tags applied to this thread. - - .. versionadded:: 2.0 - """ + """List[:class:`ForumTag`]: A list of tags applied to this thread.""" tags = [] if self.parent is None or self.parent.type != ChannelType.forum: return tags parent = self.parent for tag_id in self._applied_tags: - tag = parent.get_tag(tag_id) + tag = parent.get_tag(tag_id) # type: ignore if tag is not None: tags.append(tag) @@ -607,8 +601,6 @@ class Thread(Messageable, Hashable): A value of ``0`` disables slowmode. The maximum value possible is ``21600``. applied_tags: Sequence[:class:`ForumTag`] The new tags to apply to the thread. There can only be up to 5 tags applied to a thread. - - .. versionadded:: 2.0 reason: Optional[:class:`str`] The reason for editing this thread. Shows up on the audit log. @@ -663,8 +655,6 @@ class Thread(Messageable, Hashable): The parent channel must be a :class:`ForumChannel`. - .. versionadded:: 2.0 - Parameters ----------- \*tags: :class:`abc.Snowflake` @@ -696,8 +686,6 @@ class Thread(Messageable, Hashable): The parent channel must be a :class:`ForumChannel`. - .. versionadded:: 2.0 - Parameters ----------- \*tags: :class:`abc.Snowflake` @@ -847,8 +835,6 @@ class Thread(Messageable, Hashable): This is useful if you want to work with a message and only have its ID without doing an unnecessary API call. - .. versionadded:: 2.0 - Parameters ------------ message_id: :class:`int` diff --git a/discord/types/message.py b/discord/types/message.py index 05205c6c5..47a1b6c7c 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -86,6 +86,11 @@ class MessageReference(TypedDict, total=False): fail_if_not_exists: bool +class Call(TypedDict): + participants: List[Snowflake] + ended_timestamp: Optional[str] + + class RoleSubscriptionData(TypedDict): role_subscription_listing_id: Snowflake tier_name: str @@ -127,6 +132,7 @@ class Message(PartialMessage): interaction: NotRequired[MessageInteraction] components: NotRequired[List[Component]] position: NotRequired[int] + call: NotRequired[Call] role_subscription_data: NotRequired[RoleSubscriptionData] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index a48169cc5..1350b9d91 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -43,7 +43,7 @@ from ..user import BaseUser, User from ..flags import MessageFlags from ..asset import Asset from ..partial_emoji import PartialEmoji -from ..http import Route, handle_message_parameters, HTTPClient +from ..http import Route, handle_message_parameters, json_or_text, HTTPClient from ..mixins import Hashable from ..channel import TextChannel, ForumChannel, PartialMessageable from ..file import File @@ -800,7 +800,7 @@ class BaseWebhook(Hashable): @property def channel(self) -> Optional[Union[ForumChannel, VoiceChannel, TextChannel]]: - """Optional[Union[:class:`ForumChannel`, :class:`VoiceChannel`, :class:`TextChannel`]]: The channel this webhook belongs to. + """Optional[Union[:class:`ForumChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`TextChannel`]]: The channel this webhook belongs to. If this is a partial webhook, then this will always return ``None``. """ @@ -957,7 +957,7 @@ class Webhook(BaseWebhook): *, session: aiohttp.ClientSession = MISSING, client: Client = MISSING, - bot_token: Optional[str] = None, + user_token: Optional[str] = None, ) -> Self: """Creates a partial :class:`Webhook`. @@ -979,8 +979,8 @@ class Webhook(BaseWebhook): while this is given then the client's internal session will be used. .. versionadded:: 2.0 - bot_token: Optional[:class:`str`] - The bot authentication token for authenticated requests + user_token: Optional[:class:`str`] + The user authentication token for authenticated requests involving the webhook. .. versionadded:: 2.0 @@ -1011,7 +1011,7 @@ class Webhook(BaseWebhook): if session is MISSING: raise TypeError('session or client must be given') - return cls(data, session, token=bot_token, state=state) + return cls(data, session, token=user_token, state=state) @classmethod def from_url( @@ -1020,7 +1020,7 @@ class Webhook(BaseWebhook): *, session: aiohttp.ClientSession = MISSING, client: Client = MISSING, - bot_token: Optional[str] = None, + user_token: Optional[str] = None, ) -> Self: """Creates a partial :class:`Webhook` from a webhook URL. @@ -1044,7 +1044,7 @@ class Webhook(BaseWebhook): while this is given then the client's internal session will be used. .. versionadded:: 2.0 - bot_token: Optional[:class:`str`] + user_token: Optional[:class:`str`] The bot authentication token for authenticated requests involving the webhook. @@ -1078,7 +1078,7 @@ class Webhook(BaseWebhook): data: Dict[str, Any] = m.groupdict() data['type'] = 1 - return cls(data, session, token=bot_token, state=state) # type: ignore # Casting dict[str, Any] to WebhookPayload + return cls(data, session, token=user_token, state=state) # type: ignore # Casting dict[str, Any] to WebhookPayload @classmethod def _as_follower(cls, data, *, channel, user) -> Self: diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index cad7823fd..6952d4fad 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -615,7 +615,7 @@ class SyncWebhook(BaseWebhook): return f'https://discord.com/api/webhooks/{self.id}/{self.token}' @classmethod - def partial(cls, id: int, token: str, *, session: Session = MISSING, bot_token: Optional[str] = None) -> SyncWebhook: + def partial(cls, id: int, token: str, *, session: Session = MISSING, user_token: Optional[str] = None) -> SyncWebhook: """Creates a partial :class:`Webhook`. Parameters @@ -629,7 +629,7 @@ class SyncWebhook(BaseWebhook): that the library does not manage the session and will not close it. If not given, the ``requests`` auto session creation functions are used instead. - bot_token: Optional[:class:`str`] + user_token: Optional[:class:`str`] The bot authentication token for authenticated requests involving the webhook. @@ -651,10 +651,10 @@ class SyncWebhook(BaseWebhook): raise TypeError(f'expected requests.Session not {session.__class__!r}') else: session = requests # type: ignore - return cls(data, session, token=bot_token) + return cls(data, session, token=user_token) @classmethod - def from_url(cls, url: str, *, session: Session = MISSING, bot_token: Optional[str] = None) -> SyncWebhook: + def from_url(cls, url: str, *, session: Session = MISSING, user_token: Optional[str] = None) -> SyncWebhook: """Creates a partial :class:`Webhook` from a webhook URL. Parameters @@ -666,7 +666,7 @@ class SyncWebhook(BaseWebhook): that the library does not manage the session and will not close it. If not given, the ``requests`` auto session creation functions are used instead. - bot_token: Optional[:class:`str`] + user_token: Optional[:class:`str`] The bot authentication token for authenticated requests involving the webhook. @@ -694,7 +694,7 @@ class SyncWebhook(BaseWebhook): raise TypeError(f'expected requests.Session not {session.__class__!r}') else: session = requests # type: ignore - return cls(data, session, token=bot_token) # type: ignore + return cls(data, session, token=user_token) # type: ignore def fetch(self, *, prefer_auth: bool = True) -> SyncWebhook: """Fetches the current webhook. diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index 1a2413a12..8d11943d8 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -62,7 +62,9 @@ class WelcomeChannel: The emoji shown under the description. """ - def __init__(self, *, channel: Snowflake, description: str, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None) -> None: + def __init__( + self, *, channel: Snowflake, description: str, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None + ) -> None: self.channel = channel self.description = description @@ -129,7 +131,9 @@ class WelcomeScreen: state = self.guild._state channels = data.get('welcome_channels', []) - self.welcome_channels: List[WelcomeChannel] = [WelcomeChannel._from_dict(data=channel, state=state) for channel in channels] + self.welcome_channels: List[WelcomeChannel] = [ + WelcomeChannel._from_dict(data=channel, state=state) for channel in channels + ] self.description: str = data.get('description', '') def __repr__(self) -> str: diff --git a/docs/api.rst b/docs/api.rst index 9dd460d75..435924215 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1152,8 +1152,7 @@ Reactions .. note:: - Consider using :func:`on_raw_reaction_remove` if you need this and do not want - to enable the members intent. + Consider using :func:`on_raw_reaction_remove` if you need this and do not have a complete member cache. :param reaction: The current state of the reaction. :type reaction: :class:`Reaction` @@ -5722,9 +5721,9 @@ AuditLogDiff .. attribute:: bitrate - The bitrate of a :class:`VoiceChannel`. + The bitrate of a :class:`VoiceChannel` or :class:`StageChannel`. - See also :attr:`VoiceChannel.bitrate`. + See also :attr:`VoiceChannel.bitrate`, :attr:`StageChannel.bitrate`. :type: :class:`int` @@ -6055,12 +6054,6 @@ AuditLogDiff :type: :class:`Asset` - .. attribute:: app_command_permissions - - The permissions of the app command. - - :type: :class:`~discord.app_commands.AppCommandPermissions` - .. attribute:: enabled Whether the automod rule is active or not. @@ -7322,22 +7315,6 @@ RawEvent .. autoclass:: RawThreadDeleteEvent() :members: -PartialWebhookGuild -~~~~~~~~~~~~~~~~~~~~ - -.. attributetable:: PartialWebhookGuild - -.. autoclass:: PartialWebhookGuild() - :members: - -PartialWebhookChannel -~~~~~~~~~~~~~~~~~~~~~~~ - -.. attributetable:: PartialWebhookChannel - -.. autoclass:: PartialWebhookChannel() - :members: - .. _discord_api_data: Data Classes diff --git a/docs/faq.rst b/docs/faq.rst index e57fa6511..8340a50c9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -341,7 +341,7 @@ Quick example: Is there an event for audit log entries being created? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This event is now available in the library and Discord as of version 2.2. It can be found under :func:`on_audit_log_entry_create`. +This event is now available in the library and Discord as of version 2.0. It can be found under :func:`on_audit_log_entry_create`. Commands Extension diff --git a/docs/migrating.rst b/docs/migrating.rst index f2d27d1ed..ec34f0fed 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -887,9 +887,7 @@ Text in Voice In order to support text in voice functionality, a few changes had to be made: - :class:`VoiceChannel` is now :class:`abc.Messageable` so it can have messages sent and received. -- :attr:`Message.channel` can now be :class:`VoiceChannel`. - -In the future this may include :class:`StageChannel` when Discord implements it. +- :attr:`Message.channel` can now be :class:`VoiceChannel` and :class:`StageChannel`. Removal of ``StoreChannel`` ----------------------------- diff --git a/setup.py b/setup.py index eb49aedff..6c190caba 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ extras_require = { 'pytest-cov', 'pytest-mock', 'typing-extensions>=4.3,<5', - ] + ], } setup( @@ -63,17 +63,17 @@ setup( author='Dolfies', url='https://github.com/dolfies/discord.py-self', project_urls={ - "Documentation": "https://discordpy-self.readthedocs.io/en/latest/", - "Issue tracker": "https://github.com/dolfies/discord.py-self/issues", - "Project updates": "https://t.me/dpy_self", - "Discussion & support": "https://t.me/dpy_self_discussions", + 'Documentation': 'https://discordpy-self.readthedocs.io/en/latest/', + 'Issue tracker': 'https://github.com/dolfies/discord.py-self/issues', + 'Project updates': 'https://t.me/dpy_self', + 'Discussion & support': 'https://t.me/dpy_self_discussions', }, version=version, packages=find_packages() + [f'{prefix}.ext.commands', f'{prefix}.ext.tasks'], license='MIT', description='A Python wrapper for the Discord user API', long_description=readme, - long_description_content_type="text/x-rst", + long_description_content_type='text/x-rst', include_package_data=True, install_requires=requirements, extras_require=extras_require,