Browse Source

Implement role icons

pull/10109/head
dolfies 4 years ago
parent
commit
93b4d06f52
  1. 9
      discord/asset.py
  2. 29
      discord/guild.py
  3. 52
      discord/http.py
  4. 4
      discord/member.py
  5. 50
      discord/role.py
  6. 4
      discord/types/role.py

9
discord/asset.py

@ -246,6 +246,15 @@ class Asset(AssetMixin):
animated=animated 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: def __str__(self) -> str:
return self._url return self._url

29
discord/guild.py

@ -79,6 +79,7 @@ from .sticker import GuildSticker
from .file import File from .file import File
from .settings import GuildSettings from .settings import GuildSettings
from .profile import MemberProfile from .profile import MemberProfile
from .partial_emoji import PartialEmoji
__all__ = ( __all__ = (
@ -1437,8 +1438,6 @@ class Guild(Hashable):
community: :class:`bool` community: :class:`bool`
Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel`` Whether the guild should be a Community guild. If set to ``True``\, both ``rules_channel``
and ``public_updates_channel`` parameters are required. 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`] afk_channel: Optional[:class:`VoiceChannel`]
The new channel that is the AFK channel. Could be ``None`` for no AFK channel. The new channel that is the AFK channel. Could be ``None`` for no AFK channel.
afk_timeout: :class:`int` afk_timeout: :class:`int`
@ -1569,9 +1568,6 @@ class Guild(Hashable):
fields['owner_id'] = owner.id fields['owner_id'] = owner.id
if region is not MISSING:
fields['region'] = str(region)
if verification_level is not MISSING: if verification_level is not MISSING:
if not isinstance(verification_level, VerificationLevel): if not isinstance(verification_level, VerificationLevel):
raise InvalidArgument('verification_level field must be of type VerificationLevel') raise InvalidArgument('verification_level field must be of type VerificationLevel')
@ -2408,6 +2404,8 @@ class Guild(Hashable):
colour: Union[Colour, int] = ..., colour: Union[Colour, int] = ...,
hoist: bool = ..., hoist: bool = ...,
mentionable: bool = ..., mentionable: bool = ...,
icon: Optional[bytes] = ...,
emoji: Optional[PartialEmoji] = ...,
) -> Role: ) -> Role:
... ...
@ -2433,6 +2431,8 @@ class Guild(Hashable):
colour: Union[Colour, int] = MISSING, colour: Union[Colour, int] = MISSING,
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
icon: Optional[bytes] = MISSING,
emoji: Optional[PartialEmoji] = MISSING,
reason: Optional[str] = None, reason: Optional[str] = None,
) -> Role: ) -> Role:
"""|coro| """|coro|
@ -2462,6 +2462,11 @@ class Guild(Hashable):
mentionable: :class:`bool` mentionable: :class:`bool`
Indicates if the role should be mentionable by others. Indicates if the role should be mentionable by others.
Defaults to ``False``. 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`] reason: Optional[:class:`str`]
The reason for creating this role. Shows up on the audit log. The reason for creating this role. Shows up on the audit log.
@ -2500,6 +2505,20 @@ class Guild(Hashable):
if name is not MISSING: if name is not MISSING:
fields['name'] = name 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) data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state) role = Role(guild=self, data=data, state=self._state)

52
discord/http.py

@ -1120,22 +1120,10 @@ class HTTPClient:
# Guild management # Guild management
def get_guilds( def get_guilds(self, with_counts: bool = True) -> Response[List[guild.Guild]]:
self, params = {
limit: int,
before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None,
with_counts: bool = True
) -> Response[List[guild.Guild]]:
params: Dict[str, Snowflake] = {
'with_counts': str(with_counts).lower() '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) 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) return self.request(r, json=payload)
def get_guild(self, guild_id: Snowflake) -> Response[guild.Guild]: def get_guild(self, guild_id: Snowflake, with_counts: bool = True) -> Response[guild.Guild]:
return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id)) 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]: def delete_guild(self, guild_id: Snowflake) -> Response[None]:
return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) 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 self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any
) -> Response[role.Role]: ) -> Response[role.Role]:
r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) 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} payload = {k: v for k, v in fields.items() if k in valid_keys}
return self.request(r, json=payload, reason=reason) return self.request(r, json=payload, reason=reason)
@ -1784,7 +1776,7 @@ class HTTPClient:
# Misc # Misc
async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: 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... # And, the official clients aren't GETting it anymore, sooooo...
self.zlib = zlib self.zlib = zlib
if zlib: if zlib:
@ -1847,7 +1839,7 @@ class HTTPClient:
def delete_connection(self, type, id): def delete_connection(self, type, id):
return self.request(Route('DELETE', '/users/@me/connections/{type}/{id}', type=type, id=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 = { params = {
'with_team_applications': str(with_team_applications).lower() '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]: 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) 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): def get_partial_application(self, app_id: Snowflake):
return self.request(Route('GET', '/applications/{app_id}/rpc', app_id=app_id), auth=False) 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 def get_app_entitlements(self, app_id: Snowflake): # TODO: return type
r = Route('GET', '/users/@me/applications/{app_id}/entitlements', app_id=app_id) r = Route('GET', '/users/@me/applications/{app_id}/entitlements', app_id=app_id)
return self.request(r, super_properties_to_track=True) 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 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) 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 def mobile_report( # Report v1
self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str
): # TODO: return type ): # TODO: return type

4
discord/member.py

@ -401,7 +401,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
if original != modified: if original != modified:
to_return = User._copy(self._user) to_return = User._copy(self._user)
u.name, u._avatar, u.discriminator, u._public_flags = modified 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 return to_return, u
@property @property
@ -419,7 +419,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
@status.setter @status.setter
def status(self, value: Status) -> None: def status(self, value: Status) -> None:
# internal use only # Internal use only
self._client_status[None] = str(value) self._client_status[None] = str(value)
@property @property

50
discord/role.py

@ -25,11 +25,13 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, TypeVar, Union, TYPE_CHECKING from typing import Any, Dict, List, Optional, TypeVar, Union, TYPE_CHECKING
from .asset import Asset
from .permissions import Permissions from .permissions import Permissions
from .errors import InvalidArgument from .errors import InvalidArgument
from .colour import Colour from .colour import Colour
from .mixins import Hashable 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__ = ( __all__ = (
'RoleTags', 'RoleTags',
@ -184,6 +186,8 @@ class Role(Hashable):
'guild', 'guild',
'tags', 'tags',
'_state', '_state',
'_icon',
'_emoji',
) )
def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload): def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload):
@ -242,6 +246,8 @@ class Role(Hashable):
self.hoist: bool = data.get('hoist', False) self.hoist: bool = data.get('hoist', False)
self.managed: bool = data.get('managed', False) self.managed: bool = data.get('managed', False)
self.mentionable: bool = data.get('mentionable', 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] self.tags: Optional[RoleTags]
try: try:
@ -317,6 +323,26 @@ class Role(Hashable):
role_id = self.id role_id = self.id
return [member for member in all_members if member._roles.has(role_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: async def _move(self, position: int, reason: Optional[str]) -> None:
if position <= 0: if position <= 0:
raise InvalidArgument("Cannot move role to position 0 or below") raise InvalidArgument("Cannot move role to position 0 or below")
@ -350,6 +376,8 @@ class Role(Hashable):
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
position: int = MISSING, position: int = MISSING,
icon: Optional[bytes] = MISSING,
emoji: Optional[PartialEmoji] = MISSING,
reason: Optional[str] = MISSING, reason: Optional[str] = MISSING,
) -> Optional[Role]: ) -> Optional[Role]:
"""|coro| """|coro|
@ -382,6 +410,11 @@ class Role(Hashable):
position: :class:`int` position: :class:`int`
The new role's position. This must be below your top role's The new role's position. This must be below your top role's
position or it will fail. 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`] reason: Optional[:class:`str`]
The reason for editing this role. Shows up on the audit log. The reason for editing this role. Shows up on the audit log.
@ -394,6 +427,7 @@ class Role(Hashable):
InvalidArgument InvalidArgument
An invalid position was given or the default An invalid position was given or the default
role was asked to be moved. role was asked to be moved.
A custom emoji was passed to ``emoji``.
Returns Returns
-------- --------
@ -425,6 +459,20 @@ class Role(Hashable):
if mentionable is not MISSING: if mentionable is not MISSING:
payload['mentionable'] = mentionable 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) 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) return Role(guild=self.guild, data=data, state=self._state)

4
discord/types/role.py

@ -24,12 +24,14 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import TypedDict from typing import Optional, TypedDict
from .snowflake import Snowflake from .snowflake import Snowflake
class _RoleOptional(TypedDict, total=False): class _RoleOptional(TypedDict, total=False):
tags: RoleTags tags: RoleTags
icon: Optional[str]
unicode_emoji: Optional[str]
class Role(_RoleOptional): class Role(_RoleOptional):

Loading…
Cancel
Save