Browse Source

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

pull/9685/head
Developer Anonymous 11 months ago
parent
commit
25234fe8f3
  1. 9
      discord/app_commands/errors.py
  2. 6
      discord/appinfo.py
  3. 136
      discord/client.py
  4. 11
      discord/embeds.py
  5. 49
      discord/emoji.py
  6. 75
      discord/guild.py
  7. 75
      discord/http.py
  8. 34
      discord/member.py
  9. 6
      discord/message.py
  10. 4
      discord/poll.py
  11. 25
      discord/sticker.py
  12. 6
      discord/types/appinfo.py
  13. 2
      discord/user.py
  14. 2
      discord/utils.py
  15. 9
      discord/webhook/async_.py
  16. 1
      requirements.txt

9
discord/app_commands/errors.py

@ -485,6 +485,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,10 +497,9 @@ 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)
messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items())
if isinstance(errors, dict):
messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items())
class CommandSyncFailure(AppCommandError, HTTPException):

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 (

136
discord/client.py

@ -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
@ -2919,6 +2932,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 +3079,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 .app_commands.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

75
discord/guild.py

@ -3422,6 +3422,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,
@ -4418,6 +4449,28 @@ 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.
@ -4434,6 +4487,26 @@ class Guild(Hashable):
.. versionadded:: 2.4
"""
if not self.dms_paused_until:
return False
return 'INVITES_DISABLED' in self.features
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]:
@ -2505,7 +2517,7 @@ class HTTPClient:
),
)
# Misc
# Application
def application_info(self) -> Response[appinfo.AppInfo]:
return self.request(Route('GET', '/oauth2/applications/@me'))
@ -2526,6 +2538,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,
@ -2563,6 +2628,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.

6
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

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,

25
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,7 +201,10 @@ class StickerItem(_StickerTag):
self.name: str = data['name']
self.id: int = int(data['id'])
self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type'])
self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}'
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:
return f'<StickerItem id={self.id} name={self.name!r} format={self.format}>'
@ -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)

9
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
@ -360,7 +360,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}',
@ -1049,12 +1049,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:

1
requirements.txt

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

Loading…
Cancel
Save