Browse Source

Merge branch 'master' of https://github.com/Rapptz/discord.py

pull/9957/head
Developer Anonymous 7 months ago
parent
commit
06e5601052
  1. 6
      discord/__init__.py
  2. 27
      discord/app_commands/errors.py
  3. 6
      discord/appinfo.py
  4. 143
      discord/client.py
  5. 11
      discord/embeds.py
  6. 49
      discord/emoji.py
  7. 25
      discord/errors.py
  8. 42
      discord/flags.py
  9. 75
      discord/guild.py
  10. 75
      discord/http.py
  11. 34
      discord/member.py
  12. 10
      discord/message.py
  13. 2
      discord/permissions.py
  14. 4
      discord/poll.py
  15. 2
      discord/sku.py
  16. 6
      discord/state.py
  17. 23
      discord/sticker.py
  18. 6
      discord/types/appinfo.py
  19. 2
      discord/user.py
  20. 2
      discord/utils.py
  21. 14
      discord/webhook/async_.py
  22. 3
      docs/api.rst
  23. 5
      docs/interactions/api.rst
  24. 2
      pyproject.toml
  25. 1
      requirements.txt
  26. 7
      tests/test_permissions_all.py

6
discord/__init__.py

@ -84,4 +84,10 @@ version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel=
logging.getLogger(__name__).addHandler(logging.NullHandler())
# This is a backwards compatibility hack and should be removed in v3
# Essentially forcing the exception to have different base classes
# In the future, this should only inherit from ClientException
if len(MissingApplicationID.__bases__) == 1:
MissingApplicationID.__bases__ = (app_commands.AppCommandError, ClientException)
del logging, NamedTuple, Literal, VersionInfo

27
discord/app_commands/errors.py

@ -27,7 +27,7 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union
from ..enums import AppCommandOptionType, AppCommandType, Locale
from ..errors import DiscordException, HTTPException, _flatten_error_dict
from ..errors import DiscordException, HTTPException, _flatten_error_dict, MissingApplicationID as MissingApplicationID
from ..utils import _human_join
__all__ = (
@ -59,11 +59,6 @@ if TYPE_CHECKING:
CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu]
APP_ID_NOT_FOUND = (
'Client does not have an application_id set. Either the function was called before on_ready '
'was called or application_id was not passed to the Client constructor.'
)
class AppCommandError(DiscordException):
"""The base exception type for all application command related errors.
@ -422,19 +417,6 @@ class CommandSignatureMismatch(AppCommandError):
super().__init__(msg)
class MissingApplicationID(AppCommandError):
"""An exception raised when the client does not have an application ID set.
An application ID is required for syncing application commands.
This inherits from :exc:`~discord.app_commands.AppCommandError`.
.. versionadded:: 2.0
"""
def __init__(self, message: Optional[str] = None):
super().__init__(message or APP_ID_NOT_FOUND)
def _get_command_error(
index: str,
inner: Any,
@ -485,6 +467,10 @@ def _get_command_error(
if key == 'options':
for index, d in remaining.items():
_get_command_error(index, d, children, messages, indent=indent + 2)
elif key == '_errors':
errors = [x.get('message', '') for x in remaining]
messages.extend(f'{indentation} {message}' for message in errors)
else:
if isinstance(remaining, dict):
try:
@ -493,9 +479,8 @@ def _get_command_error(
errors = _flatten_error_dict(remaining, key=key)
else:
errors = {key: ' '.join(x.get('message', '') for x in inner_errors)}
else:
errors = _flatten_error_dict(remaining, key=key)
if isinstance(errors, dict):
messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items())

6
discord/appinfo.py

@ -147,6 +147,10 @@ class AppInfo:
The approximate count of the guilds the bot was added to.
.. versionadded:: 2.4
approximate_user_install_count: Optional[:class:`int`]
The approximate count of the user-level installations the bot has.
.. versionadded:: 2.5
"""
__slots__ = (
@ -175,6 +179,7 @@ class AppInfo:
'interactions_endpoint_url',
'redirect_uris',
'approximate_guild_count',
'approximate_user_install_count',
)
def __init__(self, state: ConnectionState, data: AppInfoPayload):
@ -212,6 +217,7 @@ class AppInfo:
self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url')
self.redirect_uris: List[str] = data.get('redirect_uris', [])
self.approximate_guild_count: int = data.get('approximate_guild_count', 0)
self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count')
def __repr__(self) -> str:
return (

143
discord/client.py

@ -84,7 +84,7 @@ if TYPE_CHECKING:
from typing_extensions import Self
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu, MissingApplicationID
from .app_commands import Command, ContextMenu
from .automod import AutoModAction, AutoModRule
from .channel import DMChannel, GroupChannel
from .ext.commands import AutoShardedBot, Bot, Context, CommandError
@ -249,6 +249,11 @@ class Client:
set to is ``30.0`` seconds.
.. versionadded:: 2.0
connector: Optional[:class:`aiohttp.BaseConnector`]
The aiohhtp connector to use for this client. This can be used to control underlying aiohttp
behavior, such as setting a dns resolver or sslcontext.
.. versionadded:: 2.5
Attributes
-----------
@ -264,6 +269,7 @@ class Client:
self.shard_id: Optional[int] = options.get('shard_id')
self.shard_count: Optional[int] = options.get('shard_count')
connector: Optional[aiohttp.BaseConnector] = options.get('connector', None)
proxy: Optional[str] = options.pop('proxy', None)
proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None)
unsync_clock: bool = options.pop('assume_unsync_clock', True)
@ -271,6 +277,7 @@ class Client:
max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None)
self.http: HTTPClient = HTTPClient(
self.loop,
connector,
proxy=proxy,
proxy_auth=proxy_auth,
unsync_clock=unsync_clock,
@ -359,7 +366,13 @@ class Client:
@property
def emojis(self) -> Sequence[Emoji]:
"""Sequence[:class:`.Emoji`]: The emojis that the connected client has."""
"""Sequence[:class:`.Emoji`]: The emojis that the connected client has.
.. note::
This not include the emojis that are owned by the application.
Use :meth:`.fetch_application_emoji` to get those.
"""
return self._connection.emojis
@property
@ -623,6 +636,11 @@ class Client:
if self._connection.application_id is None:
self._connection.application_id = self._application.id
if self._application.interactions_endpoint_url is not None:
_log.warning(
'Application has an interaction endpoint URL set, this means registered components and app commands will not be received by the library.'
)
if not self._connection.application_flags:
self._connection.application_flags = self._application.flags
@ -2919,6 +2937,33 @@ class Client:
data = await self.http.list_premium_sticker_packs()
return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']]
async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPack:
"""|coro|
Retrieves a premium sticker pack with the specified ID.
.. versionadded:: 2.5
Parameters
----------
sticker_pack_id: :class:`int`
The sticker pack's ID to fetch from.
Raises
-------
NotFound
A sticker pack with this ID does not exist.
HTTPException
Retrieving the sticker pack failed.
Returns
-------
:class:`.StickerPack`
The retrieved premium sticker pack.
"""
data = await self.http.get_sticker_pack(sticker_pack_id)
return StickerPack(state=self._connection, data=data)
async def create_dm(self, user: Snowflake) -> DMChannel:
"""|coro|
@ -3039,3 +3084,97 @@ class Client:
.. versionadded:: 2.0
"""
return self._connection.persistent_views
async def create_application_emoji(
self,
*,
name: str,
image: bytes,
) -> Emoji:
"""|coro|
Create an emoji for the current application.
.. versionadded:: 2.5
Parameters
----------
name: :class:`str`
The emoji name. Must be at least 2 characters.
image: :class:`bytes`
The :term:`py:bytes-like object` representing the image data to use.
Only JPG, PNG and GIF images are supported.
Raises
------
MissingApplicationID
The application ID could not be found.
HTTPException
Creating the emoji failed.
Returns
-------
:class:`.Emoji`
The emoji that was created.
"""
if self.application_id is None:
raise MissingApplicationID
img = utils._bytes_to_base64_data(image)
data = await self.http.create_application_emoji(self.application_id, name, img)
return Emoji(guild=Object(0), state=self._connection, data=data)
async def fetch_application_emoji(self, emoji_id: int, /) -> Emoji:
"""|coro|
Retrieves an emoji for the current application.
.. versionadded:: 2.5
Parameters
----------
emoji_id: :class:`int`
The emoji ID to retrieve.
Raises
------
MissingApplicationID
The application ID could not be found.
HTTPException
Retrieving the emoji failed.
Returns
-------
:class:`.Emoji`
The emoji requested.
"""
if self.application_id is None:
raise MissingApplicationID
data = await self.http.get_application_emoji(self.application_id, emoji_id)
return Emoji(guild=Object(0), state=self._connection, data=data)
async def fetch_application_emojis(self) -> List[Emoji]:
"""|coro|
Retrieves all emojis for the current application.
.. versionadded:: 2.5
Raises
-------
MissingApplicationID
The application ID could not be found.
HTTPException
Retrieving the emojis failed.
Returns
-------
List[:class:`.Emoji`]
The list of emojis for the current application.
"""
if self.application_id is None:
raise MissingApplicationID
data = await self.http.get_application_emojis(self.application_id)
return [Emoji(guild=Object(0), state=self._connection, data=emoji) for emoji in data['items']]

11
discord/embeds.py

@ -199,7 +199,7 @@ class Embed:
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
format that Discord expects it to be in.
You can find out about this format in the :ddocs:`official Discord documentation <resources/channel#embed-object>`.
You can find out about this format in the :ddocs:`official Discord documentation <resources/message#embed-object>`.
Parameters
-----------
@ -413,8 +413,9 @@ class Embed:
Parameters
-----------
url: :class:`str`
url: Optional[:class:`str`]
The source URL for the image. Only HTTP(S) is supported.
If ``None`` is passed, any existing image is removed.
Inline attachment URLs are also supported, see :ref:`local_image`.
"""
@ -452,13 +453,11 @@ class Embed:
This function returns the class instance to allow for fluent-style
chaining.
.. versionchanged:: 1.4
Passing ``None`` removes the thumbnail.
Parameters
-----------
url: :class:`str`
url: Optional[:class:`str`]
The source URL for the thumbnail. Only HTTP(S) is supported.
If ``None`` is passed, any existing thumbnail is removed.
Inline attachment URLs are also supported, see :ref:`local_image`.
"""

49
discord/emoji.py

@ -29,6 +29,8 @@ from .asset import Asset, AssetMixin
from .utils import SnowflakeList, snowflake_time, MISSING
from .partial_emoji import _EmojiTag, PartialEmoji
from .user import User
from .errors import MissingApplicationID
from .object import Object
# fmt: off
__all__ = (
@ -93,6 +95,10 @@ class Emoji(_EmojiTag, AssetMixin):
user: Optional[:class:`User`]
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
having :attr:`~Permissions.manage_emojis`.
Or if :meth:`.is_application_owned` is ``True``, this is the team member that uploaded
the emoji, or the bot user if it was uploaded using the API and this can
only be retrieved using :meth:`~discord.Client.fetch_application_emoji` or :meth:`~discord.Client.fetch_application_emojis`.
"""
__slots__: Tuple[str, ...] = (
@ -108,7 +114,7 @@ class Emoji(_EmojiTag, AssetMixin):
'available',
)
def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload) -> None:
def __init__(self, *, guild: Snowflake, state: ConnectionState, data: EmojiPayload) -> None:
self.guild_id: int = guild.id
self._state: ConnectionState = state
self._from_data(data)
@ -196,20 +202,32 @@ class Emoji(_EmojiTag, AssetMixin):
Deletes the custom emoji.
You must have :attr:`~Permissions.manage_emojis` to do this.
You must have :attr:`~Permissions.manage_emojis` to do this if
:meth:`.is_application_owned` is ``False``.
Parameters
-----------
reason: Optional[:class:`str`]
The reason for deleting this emoji. Shows up on the audit log.
This does not apply if :meth:`.is_application_owned` is ``True``.
Raises
-------
Forbidden
You are not allowed to delete emojis.
HTTPException
An error occurred deleting the emoji.
MissingApplicationID
The emoji is owned by an application but the application ID is missing.
"""
if self.is_application_owned():
application_id = self._state.application_id
if application_id is None:
raise MissingApplicationID
await self._state.http.delete_application_emoji(application_id, self.id)
return
await self._state.http.delete_custom_emoji(self.guild_id, self.id, reason=reason)
@ -231,15 +249,22 @@ class Emoji(_EmojiTag, AssetMixin):
The new emoji name.
roles: List[:class:`~discord.abc.Snowflake`]
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
This does not apply if :meth:`.is_application_owned` is ``True``.
reason: Optional[:class:`str`]
The reason for editing this emoji. Shows up on the audit log.
This does not apply if :meth:`.is_application_owned` is ``True``.
Raises
-------
Forbidden
You are not allowed to edit emojis.
HTTPException
An error occurred editing the emoji.
MissingApplicationID
The emoji is owned by an application but the application ID is missing
Returns
--------
@ -253,5 +278,25 @@ class Emoji(_EmojiTag, AssetMixin):
if roles is not MISSING:
payload['roles'] = [role.id for role in roles]
if self.is_application_owned():
application_id = self._state.application_id
if application_id is None:
raise MissingApplicationID
payload.pop('roles', None)
data = await self._state.http.edit_application_emoji(
application_id,
self.id,
payload=payload,
)
return Emoji(guild=Object(0), data=data, state=self._state)
data = await self._state.http.edit_custom_emoji(self.guild_id, self.id, payload=payload, reason=reason)
return Emoji(guild=self.guild, data=data, state=self._state) # type: ignore # if guild is None, the http request would have failed
def is_application_owned(self) -> bool:
""":class:`bool`: Whether the emoji is owned by an application.
.. versionadded:: 2.5
"""
return self.guild_id == 0

25
discord/errors.py

@ -47,6 +47,12 @@ __all__ = (
'ConnectionClosed',
'PrivilegedIntentsRequired',
'InteractionResponded',
'MissingApplicationID',
)
APP_ID_NOT_FOUND = (
'Client does not have an application_id set. Either the function was called before on_ready '
'was called or application_id was not passed to the Client constructor.'
)
@ -278,3 +284,22 @@ class InteractionResponded(ClientException):
def __init__(self, interaction: Interaction):
self.interaction: Interaction = interaction
super().__init__('This interaction has already been responded to before')
class MissingApplicationID(ClientException):
"""An exception raised when the client does not have an application ID set.
An application ID is required for syncing application commands and various
other application tasks such as SKUs or application emojis.
This inherits from :exc:`~discord.app_commands.AppCommandError`
and :class:`~discord.ClientException`.
.. versionadded:: 2.0
.. versionchanged:: 2.5
This is now exported to the ``discord`` namespace and now inherits from :class:`~discord.ClientException`.
"""
def __init__(self, message: Optional[str] = None):
super().__init__(message or APP_ID_NOT_FOUND)

42
discord/flags.py

@ -2032,6 +2032,48 @@ class MemberFlags(BaseFlags):
""":class:`bool`: Returns ``True`` if the member has started onboarding."""
return 1 << 3
@flag_value
def guest(self):
""":class:`bool`: Returns ``True`` if the member is a guest and can only access
the voice channel they were invited to.
.. versionadded:: 2.5
"""
return 1 << 4
@flag_value
def started_home_actions(self):
""":class:`bool`: Returns ``True`` if the member has started Server Guide new member actions.
.. versionadded:: 2.5
"""
return 1 << 5
@flag_value
def completed_home_actions(self):
""":class:`bool`: Returns ``True`` if the member has completed Server Guide new member actions.
.. versionadded:: 2.5
"""
return 1 << 6
@flag_value
def automod_quarantined_username(self):
""":class:`bool`: Returns ``True`` if the member's username, nickname, or global name has been
blocked by AutoMod.
.. versionadded:: 2.5
"""
return 1 << 7
@flag_value
def dm_settings_upsell_acknowledged(self):
""":class:`bool`: Returns ``True`` if the member has dismissed the DM settings upsell.
.. versionadded:: 2.5
"""
return 1 << 9
@fill_with_flags()
class AttachmentFlags(BaseFlags):

75
discord/guild.py

@ -3408,6 +3408,37 @@ class Guild(Hashable):
data = await self._state.http.get_roles(self.id)
return [Role(guild=self, state=self._state, data=d) for d in data]
async def fetch_role(self, role_id: int, /) -> Role:
"""|coro|
Retrieves a :class:`Role` with the specified ID.
.. versionadded:: 2.5
.. note::
This method is an API call. For general usage, consider :attr:`get_role` instead.
Parameters
----------
role_id: :class:`int`
The role's ID.
Raises
-------
NotFound
The role requested could not be found.
HTTPException
An error occurred fetching the role.
Returns
-------
:class:`Role`
The retrieved role.
"""
data = await self._state.http.get_role(self.id, role_id)
return Role(guild=self, state=self._state, data=data)
@overload
async def create_role(
self,
@ -4404,13 +4435,35 @@ class Guild(Hashable):
return utils.parse_time(self._incidents_data.get('dms_disabled_until'))
@property
def dm_spam_detected_at(self) -> Optional[datetime.datetime]:
""":class:`datetime.datetime`: Returns the time when DM spam was detected in the guild.
.. versionadded:: 2.5
"""
if not self._incidents_data:
return None
return utils.parse_time(self._incidents_data.get('dm_spam_detected_at'))
@property
def raid_detected_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild.
.. versionadded:: 2.5
"""
if not self._incidents_data:
return None
return utils.parse_time(self._incidents_data.get('raid_detected_at'))
def invites_paused(self) -> bool:
""":class:`bool`: Whether invites are paused in the guild.
.. versionadded:: 2.4
"""
if not self.invites_paused_until:
return False
return 'INVITES_DISABLED' in self.features
return self.invites_paused_until > utils.utcnow()
@ -4423,3 +4476,23 @@ class Guild(Hashable):
return False
return self.dms_paused_until > utils.utcnow()
def is_dm_spam_detected(self) -> bool:
""":class:`bool`: Whether DM spam was detected in the guild.
.. versionadded:: 2.5
"""
if not self.dm_spam_detected_at:
return False
return self.dm_spam_detected_at > utils.utcnow()
def is_raid_detected(self) -> bool:
""":class:`bool`: Whether a raid was detected in the guild.
.. versionadded:: 2.5
"""
if not self.raid_detected_at:
return False
return self.raid_detected_at > utils.utcnow()

75
discord/http.py

@ -48,7 +48,6 @@ from typing import (
from urllib.parse import quote as _uriquote
from collections import deque
import datetime
import socket
import aiohttp
@ -93,6 +92,7 @@ if TYPE_CHECKING:
welcome_screen,
sku,
poll,
voice,
)
from .types.snowflake import Snowflake, SnowflakeList
@ -798,13 +798,13 @@ class HTTPClient:
async def static_login(self, token: str) -> user.User:
# Necessary to get aiohttp to stop complaining about session creation
if self.connector is MISSING:
# discord does not support ipv6
self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET)
self.connector = aiohttp.TCPConnector(limit=0)
self.__session = aiohttp.ClientSession(
connector=self.connector,
ws_response_class=DiscordClientWebSocketResponse,
trace_configs=None if self.http_trace is None else [self.http_trace],
cookie_jar=aiohttp.DummyCookieJar(),
)
self._global_over = asyncio.Event()
self._global_over.set()
@ -1148,6 +1148,12 @@ class HTTPClient:
r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id)
return self.request(r, json=fields, reason=reason)
def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]:
return self.request(Route('GET', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id))
def get_voice_state(self, guild_id: Snowflake, user_id: Snowflake) -> Response[voice.GuildVoiceState]:
return self.request(Route('GET', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id))
# Channel management
def edit_channel(
@ -1611,6 +1617,9 @@ class HTTPClient:
def get_sticker(self, sticker_id: Snowflake) -> Response[sticker.Sticker]:
return self.request(Route('GET', '/stickers/{sticker_id}', sticker_id=sticker_id))
def get_sticker_pack(self, sticker_pack_id: Snowflake) -> Response[sticker.StickerPack]:
return self.request(Route('GET', '/sticker-packs/{sticker_pack_id}', sticker_pack_id=sticker_pack_id))
def list_premium_sticker_packs(self) -> Response[sticker.ListPremiumStickerPacks]:
return self.request(Route('GET', '/sticker-packs'))
@ -1858,6 +1867,9 @@ class HTTPClient:
def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]:
return self.request(Route('GET', '/guilds/{guild_id}/roles', guild_id=guild_id))
def get_role(self, guild_id: Snowflake, role_id: Snowflake) -> Response[role.Role]:
return self.request(Route('GET', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id))
def edit_role(
self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any
) -> Response[role.Role]:
@ -2503,7 +2515,7 @@ class HTTPClient:
),
)
# Misc
# Application
def application_info(self) -> Response[appinfo.AppInfo]:
return self.request(Route('GET', '/oauth2/applications/@me'))
@ -2524,6 +2536,59 @@ class HTTPClient:
payload = {k: v for k, v in payload.items() if k in valid_keys}
return self.request(Route('PATCH', '/applications/@me'), json=payload, reason=reason)
def get_application_emojis(self, application_id: Snowflake) -> Response[appinfo.ListAppEmojis]:
return self.request(Route('GET', '/applications/{application_id}/emojis', application_id=application_id))
def get_application_emoji(self, application_id: Snowflake, emoji_id: Snowflake) -> Response[emoji.Emoji]:
return self.request(
Route(
'GET', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id
)
)
def create_application_emoji(
self,
application_id: Snowflake,
name: str,
image: str,
) -> Response[emoji.Emoji]:
payload = {
'name': name,
'image': image,
}
return self.request(
Route('POST', '/applications/{application_id}/emojis', application_id=application_id), json=payload
)
def edit_application_emoji(
self,
application_id: Snowflake,
emoji_id: Snowflake,
*,
payload: Dict[str, Any],
) -> Response[emoji.Emoji]:
r = Route(
'PATCH', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id
)
return self.request(r, json=payload)
def delete_application_emoji(
self,
application_id: Snowflake,
emoji_id: Snowflake,
) -> Response[None]:
return self.request(
Route(
'DELETE',
'/applications/{application_id}/emojis/{emoji_id}',
application_id=application_id,
emoji_id=emoji_id,
)
)
# Poll
def get_poll_answer_voters(
self,
channel_id: Snowflake,
@ -2561,6 +2626,8 @@ class HTTPClient:
)
)
# Misc
async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str:
try:
data = await self.request(Route('GET', '/gateway'))

34
discord/member.py

@ -1153,6 +1153,40 @@ class Member(discord.abc.Messageable, _UserTag):
for role in roles:
await req(guild_id, user_id, role.id, reason=reason)
async def fetch_voice(self) -> VoiceState:
"""|coro|
Retrieves the current voice state from this member.
.. versionadded:: 2.5
Raises
-------
NotFound
The member is not in a voice channel.
Forbidden
You do not have permissions to get a voice state.
HTTPException
Retrieving the voice state failed.
Returns
-------
:class:`VoiceState`
The current voice state of the member.
"""
guild_id = self.guild.id
if self._state.self_id == self.id:
data = await self._state.http.get_my_voice_state(guild_id)
else:
data = await self._state.http.get_voice_state(guild_id, self.id)
channel_id = data.get('channel_id')
channel: Optional[VocalGuildChannel] = None
if channel_id is not None:
channel = self.guild.get_channel(int(channel_id)) # type: ignore # must be voice channel here
return VoiceState(data=data, channel=channel)
def get_role(self, role_id: int, /) -> Optional[Role]:
"""Returns a role with the given ID from roles which the member has.

10
discord/message.py

@ -194,6 +194,10 @@ class Attachment(Hashable):
The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
title: Optional[:class:`str`]
The normalised version of the attachment's filename.
.. versionadded:: 2.5
"""
__slots__ = (
@ -211,6 +215,7 @@ class Attachment(Hashable):
'duration',
'waveform',
'_flags',
'title',
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
@ -226,6 +231,7 @@ class Attachment(Hashable):
self.description: Optional[str] = data.get('description')
self.ephemeral: bool = data.get('ephemeral', False)
self.duration: Optional[float] = data.get('duration_secs')
self.title: Optional[str] = data.get('title')
waveform = data.get('waveform')
self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None
@ -1837,13 +1843,11 @@ class Message(PartialMessage, Hashable):
self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id')
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
# This updates the poll so it has the counts, if the message
# was previously cached.
self.poll: Optional[Poll] = None
try:
self.poll = Poll._from_data(data=data['poll'], message=self, state=state)
except KeyError:
self.poll = state._get_poll(self.id)
pass
try:
# if the channel doesn't have a guild attribute, we handle that

2
discord/permissions.py

@ -187,7 +187,7 @@ class Permissions(BaseFlags):
permissions set to ``True``.
"""
# Some of these are 0 because we don't want to set unnecessary bits
return cls(0b0000_0000_0000_0010_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
@classmethod
def _timeout_mask(cls) -> int:

4
discord/poll.py

@ -384,9 +384,9 @@ class Poll:
question_data = data.get('question')
question = question_data.get('text')
expiry = utils.parse_time(data['expiry']) # If obtained via API, then expiry is set.
duration = expiry - message.created_at
# expiry - message.created_at may be a few nanos away from the actual duration
duration = datetime.timedelta(hours=round((expiry - message.created_at).total_seconds() / 3600))
# self.created_at = message.created_at
# duration = self.created_at - expiry
self = cls(
duration=duration,

2
discord/sku.py

@ -28,7 +28,7 @@ from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from . import utils
from .app_commands import MissingApplicationID
from .errors import MissingApplicationID
from .enums import try_enum, SKUType, EntitlementType
from .flags import SKUFlags

6
discord/state.py

@ -510,12 +510,6 @@ class ConnectionState(Generic[ClientT]):
def _get_message(self, msg_id: Optional[int]) -> Optional[Message]:
return utils.find(lambda m: m.id == msg_id, reversed(self._messages)) if self._messages else None
def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]:
message = self._get_message(msg_id)
if not message:
return
return message.poll
def _add_guild_from_data(self, data: GuildPayload) -> Guild:
guild = Guild(data=data, state=self)
self._add_guild(guild)

23
discord/sticker.py

@ -28,8 +28,7 @@ import unicodedata
from .mixins import Hashable
from .asset import Asset, AssetMixin
from .utils import cached_slot_property, find, snowflake_time, get, MISSING, _get_as_snowflake
from .errors import InvalidData
from .utils import cached_slot_property, snowflake_time, get, MISSING, _get_as_snowflake
from .enums import StickerType, StickerFormatType, try_enum
__all__ = (
@ -51,7 +50,6 @@ if TYPE_CHECKING:
Sticker as StickerPayload,
StandardSticker as StandardStickerPayload,
GuildSticker as GuildStickerPayload,
ListPremiumStickerPacks as ListPremiumStickerPacksPayload,
)
@ -203,6 +201,9 @@ class StickerItem(_StickerTag):
self.name: str = data['name']
self.id: int = int(data['id'])
self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type'])
if self.format is StickerFormatType.gif:
self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif'
else:
self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}'
def __repr__(self) -> str:
@ -258,8 +259,6 @@ class Sticker(_StickerTag):
The id of the sticker.
description: :class:`str`
The description of the sticker.
pack_id: :class:`int`
The id of the sticker's pack.
format: :class:`StickerFormatType`
The format for the sticker's image.
url: :class:`str`
@ -352,9 +351,12 @@ class StandardSticker(Sticker):
Retrieves the sticker pack that this sticker belongs to.
.. versionchanged:: 2.5
Now raises ``NotFound`` instead of ``InvalidData``.
Raises
--------
InvalidData
NotFound
The corresponding sticker pack was not found.
HTTPException
Retrieving the sticker pack failed.
@ -364,13 +366,8 @@ class StandardSticker(Sticker):
:class:`StickerPack`
The retrieved sticker pack.
"""
data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs()
packs = data['sticker_packs']
pack = find(lambda d: int(d['id']) == self.pack_id, packs)
if pack:
return StickerPack(state=self._state, data=pack)
raise InvalidData(f'Could not find corresponding sticker pack for {self!r}')
data = await self._state.http.get_sticker_pack(self.pack_id)
return StickerPack(state=self._state, data=data)
class GuildSticker(Sticker):

6
discord/types/appinfo.py

@ -30,6 +30,7 @@ from typing_extensions import NotRequired
from .user import User
from .team import Team
from .snowflake import Snowflake
from .emoji import Emoji
class InstallParams(TypedDict):
@ -45,6 +46,7 @@ class BaseAppInfo(TypedDict):
summary: str
description: str
flags: int
approximate_user_install_count: NotRequired[int]
cover_image: NotRequired[str]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]
@ -78,3 +80,7 @@ class PartialAppInfo(BaseAppInfo, total=False):
class GatewayAppInfo(TypedDict):
id: Snowflake
flags: int
class ListAppEmojis(TypedDict):
items: List[Emoji]

2
discord/user.py

@ -171,7 +171,7 @@ class BaseUser(_UserTag):
@property
def default_avatar(self) -> Asset:
""":class:`Asset`: Returns the default avatar for a given user."""
if self.discriminator == '0':
if self.discriminator in ('0', '0000'):
avatar_id = (self.id >> 22) % len(DefaultAvatar)
else:
avatar_id = int(self.discriminator) % 5

2
discord/utils.py

@ -302,7 +302,7 @@ def deprecated(instead: Optional[str] = None) -> Callable[[Callable[P, T]], Call
else:
fmt = '{0.__name__} is deprecated.'
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
warnings.warn(fmt.format(func, instead), stacklevel=2, category=DeprecationWarning)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)

14
discord/webhook/async_.py

@ -38,7 +38,7 @@ import aiohttp
from .. import utils
from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError
from ..message import Message
from ..enums import try_enum, WebhookType, ChannelType
from ..enums import try_enum, WebhookType, ChannelType, DefaultAvatar
from ..user import BaseUser, User
from ..flags import MessageFlags
from ..asset import Asset
@ -363,7 +363,7 @@ class AsyncWebhookAdapter:
multipart: Optional[List[Dict[str, Any]]] = None,
files: Optional[Sequence[File]] = None,
thread_id: Optional[int] = None,
) -> Response[Message]:
) -> Response[MessagePayload]:
route = Route(
'PATCH',
'/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}',
@ -736,11 +736,6 @@ class _WebhookState:
return self._parent._get_guild(guild_id)
return None
def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]:
if self._parent is not None:
return self._parent._get_poll(msg_id)
return None
def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser:
if self._parent is not None:
return self._parent.store_user(data, cache=cache)
@ -1064,12 +1059,11 @@ class BaseWebhook(Hashable):
@property
def default_avatar(self) -> Asset:
"""
:class:`Asset`: Returns the default avatar. This is always the blurple avatar.
:class:`Asset`: Returns the default avatar.
.. versionadded:: 2.0
"""
# Default is always blurple apparently
return Asset._from_default_avatar(self._state, 0)
return Asset._from_default_avatar(self._state, (self.id >> 22) % len(DefaultAvatar))
@property
def display_avatar(self) -> Asset:

3
docs/api.rst

@ -5440,6 +5440,8 @@ The following exceptions are thrown by the library.
.. autoexception:: InteractionResponded
.. autoexception:: MissingApplicationID
.. autoexception:: discord.opus.OpusError
.. autoexception:: discord.opus.OpusNotLoaded
@ -5457,6 +5459,7 @@ Exception Hierarchy
- :exc:`ConnectionClosed`
- :exc:`PrivilegedIntentsRequired`
- :exc:`InteractionResponded`
- :exc:`MissingApplicationID`
- :exc:`GatewayNotFound`
- :exc:`HTTPException`
- :exc:`Forbidden`

5
docs/interactions/api.rst

@ -872,9 +872,6 @@ Exceptions
.. autoexception:: discord.app_commands.CommandNotFound
:members:
.. autoexception:: discord.app_commands.MissingApplicationID
:members:
.. autoexception:: discord.app_commands.CommandSyncFailure
:members:
@ -899,7 +896,7 @@ Exception Hierarchy
- :exc:`~discord.app_commands.CommandAlreadyRegistered`
- :exc:`~discord.app_commands.CommandSignatureMismatch`
- :exc:`~discord.app_commands.CommandNotFound`
- :exc:`~discord.app_commands.MissingApplicationID`
- :exc:`~discord.MissingApplicationID`
- :exc:`~discord.app_commands.CommandSyncFailure`
- :exc:`~discord.HTTPException`
- :exc:`~discord.app_commands.CommandSyncFailure`

2
pyproject.toml

@ -53,7 +53,7 @@ docs = [
]
speed = [
"orjson>=3.5.4",
"aiodns>=1.1",
"aiodns>=1.1; sys_platform != 'win32'",
"Brotli",
"cchardet==2.1.7; python_version < '3.10'",
]

1
requirements.txt

@ -1 +1,2 @@
aiohttp>=3.7.4,<4
audioop-lts; python_version>='3.13'

7
tests/test_permissions_all.py

@ -0,0 +1,7 @@
import discord
from functools import reduce
from operator import or_
def test_permissions_all():
assert discord.Permissions.all().value == reduce(or_, discord.Permissions.VALID_FLAGS.values())
Loading…
Cancel
Save