diff --git a/discord/asset.py b/discord/asset.py index c58cfd364..b0732a692 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -246,6 +246,15 @@ class Asset(AssetMixin): animated=animated ) + @classmethod + def _from_role_icon(cls, state, role_id: int, icon_hash: str) -> Asset: + return cls( + state, + url=f'{cls.BASE}/role-icons/{role_id}/{icon_hash}.png', + key=icon_hash, + animated=False, + ) + def __str__(self) -> str: return self._url diff --git a/discord/guild.py b/discord/guild.py index 6af86b4e0..b6b1fc4ba 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -79,6 +79,7 @@ from .sticker import GuildSticker from .file import File from .settings import GuildSettings from .profile import MemberProfile +from .partial_emoji import PartialEmoji __all__ = ( @@ -1437,8 +1438,6 @@ class Guild(Hashable): community: :class:`bool` Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel`` and ``public_updates_channel`` parameters are required. - region: Union[:class:`str`, :class:`VoiceRegion`] - The new region for the guild's voice communication. afk_channel: Optional[:class:`VoiceChannel`] The new channel that is the AFK channel. Could be ``None`` for no AFK channel. afk_timeout: :class:`int` @@ -1569,9 +1568,6 @@ class Guild(Hashable): fields['owner_id'] = owner.id - if region is not MISSING: - fields['region'] = str(region) - if verification_level is not MISSING: if not isinstance(verification_level, VerificationLevel): raise InvalidArgument('verification_level field must be of type VerificationLevel') @@ -2408,6 +2404,8 @@ class Guild(Hashable): colour: Union[Colour, int] = ..., hoist: bool = ..., mentionable: bool = ..., + icon: Optional[bytes] = ..., + emoji: Optional[PartialEmoji] = ..., ) -> Role: ... @@ -2433,6 +2431,8 @@ class Guild(Hashable): colour: Union[Colour, int] = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, + icon: Optional[bytes] = MISSING, + emoji: Optional[PartialEmoji] = MISSING, reason: Optional[str] = None, ) -> Role: """|coro| @@ -2462,6 +2462,11 @@ class Guild(Hashable): mentionable: :class:`bool` Indicates if the role should be mentionable by others. Defaults to ``False``. + icon: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG is supported. + Could be ``None`` to denote removal of the icon. + emoji: Optional[:class:`PartialEmoji`] + An emoji to show next to the role. Only unicode emojis are supported. reason: Optional[:class:`str`] The reason for creating this role. Shows up on the audit log. @@ -2500,6 +2505,20 @@ class Guild(Hashable): if name is not MISSING: fields['name'] = name + if icon is not MISSING: + if icon is None: + fields['icon'] = icon + else: + fields['icon'] = utils._bytes_to_base64_data(icon) + + if emoji is not MISSING: + if emoji is None: + fields['unicode_emoji'] = None + elif emoji.id is not None: + raise InvalidArgument('emoji only supports unicode emojis') + else: + fields['unicode_emoji'] = emoji.name + data = await self._state.http.create_role(self.id, reason=reason, **fields) role = Role(guild=self, data=data, state=self._state) diff --git a/discord/http.py b/discord/http.py index c00bf38f1..292de58ed 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1120,22 +1120,10 @@ class HTTPClient: # Guild management - def get_guilds( - self, - limit: int, - before: Optional[Snowflake] = None, - after: Optional[Snowflake] = None, - with_counts: bool = True - ) -> Response[List[guild.Guild]]: - params: Dict[str, Snowflake] = { + def get_guilds(self, with_counts: bool = True) -> Response[List[guild.Guild]]: + params = { 'with_counts': str(with_counts).lower() } - if limit and limit != 200: - params['limit'] = limit - if before: - params['before'] = before - if after: - params['after'] = after return self.request(Route('GET', '/users/@me/guilds'), params=params, super_properties_to_track=True) @@ -1147,8 +1135,12 @@ class HTTPClient: return self.request(r, json=payload) - def get_guild(self, guild_id: Snowflake) -> Response[guild.Guild]: - return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id)) + def get_guild(self, guild_id: Snowflake, with_counts: bool = True) -> Response[guild.Guild]: + params = { + 'with_counts': str(with_counts).lower() + } + + return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id), params=params) def delete_guild(self, guild_id: Snowflake) -> Response[None]: return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) @@ -1564,7 +1556,7 @@ class HTTPClient: self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any ) -> Response[role.Role]: r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) - valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') + valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable', 'icon', 'unicode_emoji') payload = {k: v for k, v in fields.items() if k in valid_keys} return self.request(r, json=payload, reason=reason) @@ -1784,7 +1776,7 @@ class HTTPClient: # Misc async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: - # The gateway URL hasn't changed for over 5 years + # The gateway URL hasn't changed for over 5 years # And, the official clients aren't GETting it anymore, sooooo... self.zlib = zlib if zlib: @@ -1847,7 +1839,7 @@ class HTTPClient: def delete_connection(self, type, id): return self.request(Route('DELETE', '/users/@me/connections/{type}/{id}', type=type, id=id)) - def get_applications(self, *, with_team_applications: bool = True) -> Response[List[appinfo.AppInfo]]: + def get_my_applications(self, *, with_team_applications: bool = True) -> Response[List[appinfo.AppInfo]]: params = { 'with_team_applications': str(with_team_applications).lower() } @@ -1857,9 +1849,22 @@ class HTTPClient: def get_my_application(self, app_id: Snowflake) -> Response[appinfo.AppInfo]: return self.request(Route('GET', '/applications/{app_id}', app_id=app_id), super_properties_to_track=True) + def edit_application(self, app_id: Snowflake, payload) -> Response[appinfo.AppInfo]: + return self.request(Route('PATCH', '/applications/{app_id}', app_id=app_id), super_properties_to_track=True, json=payload) + + def delete_application(self, app_id: Snowflake) -> Response[appinfo.AppInfo]: + return self.request(Route('POST', '/applications/{app_id}/delete', app_id=app_id), super_properties_to_track=True) + def get_partial_application(self, app_id: Snowflake): return self.request(Route('GET', '/applications/{app_id}/rpc', app_id=app_id), auth=False) + def create_app(self, name: str): + payload = { + 'name': name + } + + return self.request(Route('POST', '/applications'), json=payload) + def get_app_entitlements(self, app_id: Snowflake): # TODO: return type r = Route('GET', '/users/@me/applications/{app_id}/entitlements', app_id=app_id) return self.request(r, super_properties_to_track=True) @@ -1884,6 +1889,15 @@ class HTTPClient: def get_team(self, team_id: Snowflake): # TODO: return type return self.request(Route('GET', '/teams/{team_id}', team_id=team_id), super_properties_to_track=True) + def botify_app(self, app_id: Snowflake): + return self.request(Route('POST', '/applications/{app_id}/bot', app_id=app_id), super_properties_to_track=True) + + def reset_secret(self, app_id: Snowflake) -> Response[appinfo.AppInfo]: + return self.request(Route('POST', '/applications/{app_id}/reset', app_id=app_id), super_properties_to_track=True) + + def reset_token(self, app_id: Snowflake): + return self.request(Route('POST', '/applications/{app_id}/bot/reset', app_id=app_id), super_properties_to_track=True) + def mobile_report( # Report v1 self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str ): # TODO: return type diff --git a/discord/member.py b/discord/member.py index ac8d6d19a..fc620b671 100644 --- a/discord/member.py +++ b/discord/member.py @@ -401,7 +401,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): if original != modified: to_return = User._copy(self._user) u.name, u._avatar, u.discriminator, u._public_flags = modified - # Signal to dispatch on_user_update + # Signal to dispatch user_update return to_return, u @property @@ -419,7 +419,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): @status.setter def status(self, value: Status) -> None: - # internal use only + # Internal use only self._client_status[None] = str(value) @property diff --git a/discord/role.py b/discord/role.py index 1ebc1f614..ab1502e66 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,11 +25,13 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations from typing import Any, Dict, List, Optional, TypeVar, Union, TYPE_CHECKING +from .asset import Asset from .permissions import Permissions from .errors import InvalidArgument from .colour import Colour from .mixins import Hashable -from .utils import snowflake_time, _get_as_snowflake, MISSING +from .utils import snowflake_time, _get_as_snowflake, MISSING, _bytes_to_base64_data +from .partial_emoji import PartialEmoji __all__ = ( 'RoleTags', @@ -184,6 +186,8 @@ class Role(Hashable): 'guild', 'tags', '_state', + '_icon', + '_emoji', ) def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): @@ -242,6 +246,8 @@ class Role(Hashable): self.hoist: bool = data.get('hoist', False) self.managed: bool = data.get('managed', False) self.mentionable: bool = data.get('mentionable', False) + self._icon: Optional[str] = data.get('icon') + self._emoji: Optional[str] = data.get('unicode_emoji') self.tags: Optional[RoleTags] try: @@ -317,6 +323,26 @@ class Role(Hashable): role_id = self.id return [member for member in all_members if member._roles.has(role_id)] + @property + def icon(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the role's icon asset, if available. + + .. versionadded:: 2.0 + """ + if (icon := self._icon) is None: + return + return Asset._from_role_icon(self._state, self.id, icon) + + @property + def emoji(self) -> Optional[PartialEmoji]: + """Optional[:class:`PartialEmoji`] Returns the role's unicode emoji, if available. + + .. versionadded:: 2.0 + """ + if (emoji := self._emoji) is None: + return + return PartialEmoji.from_str(emoji) + async def _move(self, position: int, reason: Optional[str]) -> None: if position <= 0: raise InvalidArgument("Cannot move role to position 0 or below") @@ -350,6 +376,8 @@ class Role(Hashable): hoist: bool = MISSING, mentionable: bool = MISSING, position: int = MISSING, + icon: Optional[bytes] = MISSING, + emoji: Optional[PartialEmoji] = MISSING, reason: Optional[str] = MISSING, ) -> Optional[Role]: """|coro| @@ -382,6 +410,11 @@ class Role(Hashable): position: :class:`int` The new role's position. This must be below your top role's position or it will fail. + icon: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG is supported. + Could be ``None`` to denote removal of the icon. + emoji: Optional[:class:`PartialEmoji`] + An emoji to show next to the role. Only unicode emojis are supported. reason: Optional[:class:`str`] The reason for editing this role. Shows up on the audit log. @@ -394,6 +427,7 @@ class Role(Hashable): InvalidArgument An invalid position was given or the default role was asked to be moved. + A custom emoji was passed to ``emoji``. Returns -------- @@ -425,6 +459,20 @@ class Role(Hashable): if mentionable is not MISSING: payload['mentionable'] = mentionable + if icon is not MISSING: + if icon is None: + payload['icon'] = icon + else: + payload['icon'] = _bytes_to_base64_data(icon) + + if emoji is not MISSING: + if emoji is None: + payload['unicode_emoji'] = None + elif emoji.id is not None: + raise InvalidArgument('emoji only supports unicode emojis') + else: + payload['unicode_emoji'] = emoji.name + data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) diff --git a/discord/types/role.py b/discord/types/role.py index aba7b1bda..105a8c89c 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -24,12 +24,14 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import TypedDict +from typing import Optional, TypedDict from .snowflake import Snowflake class _RoleOptional(TypedDict, total=False): tags: RoleTags + icon: Optional[str] + unicode_emoji: Optional[str] class Role(_RoleOptional):