Browse Source

Add support for partial roles

pull/10386/head
Soheab 5 months ago
parent
commit
3b8ce4ddec
  1. 176
      discord/invite.py
  2. 4
      discord/types/invite.py
  3. 11
      discord/types/role.py
  4. 8
      docs/api.rst

176
discord/invite.py

@ -41,13 +41,15 @@ from .enums import (
from .appinfo import PartialAppInfo
from .scheduled_event import ScheduledEvent
from .flags import InviteFlags
from .role import Role
from .permissions import Permissions
from .colour import Colour
__all__ = (
'PartialInviteChannel',
'PartialInviteGuild',
'Invite',
'InviteUsersJob',
'PartialInviteRole',
)
if TYPE_CHECKING:
@ -58,6 +60,7 @@ if TYPE_CHECKING:
InviteGuild as InviteGuildPayload,
GatewayInvite as GatewayInvitePayload,
InviteTargetUsersJobStatus as InviteTargetUsersJobStatusPayload,
InviteRole as InviteRolePayload,
)
from .types.guild import GuildFeature
from .types.channel import (
@ -68,6 +71,7 @@ if TYPE_CHECKING:
from .abc import GuildChannel
from .user import User
from .abc import Snowflake
from .role import Role
InviteGuildType = Union[Guild, 'PartialInviteGuild', Object]
InviteChannelType = Union[GuildChannel, 'PartialInviteChannel', Object]
@ -111,6 +115,116 @@ class InviteUsersJob:
)
class PartialInviteRole:
"""Represents a "partial" invite role.
This model will be given when the bot is not part of the
guild the :class:`Invite` resolves to, or when the role
is not in cache.
.. versionadded:: 2.7
"""
__slots__ = (
'id',
'name',
'position',
'unicode_emoji',
'_colour',
'_secondary_colour',
'_tertiary_colour',
'_icon',
'_permissions',
'_state',
)
def __init__(self, state: ConnectionState, data: InviteRolePayload) -> None:
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.name: str = data['name']
self.position: int = data.get('position', 0)
colors = data.get('colors', {})
self._colour: int = colors.get('primary_color', 0)
self._secondary_colour = colors.get('secondary_color', None)
self._tertiary_colour = colors.get('tertiary_color', None)
self.unicode_emoji: Optional[str] = data.get('unicode_emoji')
self._icon: Optional[str] = data.get('icon')
self._permissions: int = int(data.get('permissions', 0))
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f'<Role id={self.id} name={self.name!r}>'
@property
def secondary_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: The role's secondary colour."""
return Colour(self._secondary_colour) if self._secondary_colour is not None else None
@property
def secondary_color(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: Alias for :attr:`secondary_colour`."""
return self.secondary_colour
@property
def tertiary_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: The role's tertiary colour."""
return Colour(self._tertiary_colour) if self._tertiary_colour is not None else None
@property
def tertiary_color(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: Alias for :attr:`tertiary_colour`."""
return self.tertiary_colour
@property
def permissions(self) -> Permissions:
""":class:`Permissions`: Returns the role's permissions."""
return Permissions(self._permissions)
@property
def colour(self) -> Colour:
""":class:`Colour`: Returns the role's primary colour. An alias exists under ``color``."""
return Colour(self._colour)
@property
def color(self) -> Colour:
""":class:`Colour`: Returns the role's primary colour. An alias exists under ``colour``."""
return self.colour
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Returns the role's icon asset, if available.
.. note::
If this is ``None``, the role might instead have unicode emoji as its icon
if :attr:`unicode_emoji` is not ``None``.
If you want the icon that a role has displayed, consider using :attr:`display_icon`.
"""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path='role')
@property
def display_icon(self) -> Optional[Union[Asset, str]]:
"""Optional[Union[:class:`.Asset`, :class:`str`]]: Returns the role's display icon, if available."""
return self.icon or self.unicode_emoji
@property
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the role's creation time in UTC."""
return snowflake_time(self.id)
@property
def mention(self) -> str:
""":class:`str`: Returns a string that allows you to mention a role."""
return f'<@&{self.id}>'
class PartialInviteChannel:
"""Represents a "partial" invite channel.
@ -405,11 +519,14 @@ class Invite(Hashable):
The ID of the scheduled event associated with this invite, if any.
.. versionadded:: 2.0
roles: List[Union[:class:`Role`, :class:`Object`]]
roles: List[Union[:class:`PartialInviteRole`, :class:`Role`, :class:`Object`]]
A list of roles that are granted to users joining via this invite.
This is only filled if the bot is in the guild where the invite belongs.
This may contain :class:`Object` instances if the role is not cached.
Objects in this list may be...
- :class:`Role` if the bot is part of the guild this invite resolves to and the role is in cache.
- :class:`PartialInviteRole` if the invite is fetched through :meth:`Client.fetch_invite` and
the bot is not in the guild the invite resolves to, or if the role is not in cache.
- :class:`Object` if the invite is received through a gateway event or the role is not in cache.
.. versionadded:: 2.7
"""
@ -492,12 +609,9 @@ class Invite(Hashable):
self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None
self._flags: int = data.get('flags', 0)
roles = data.get('roles', [])
self.roles: List[Union[Role, Object]]
if roles and self.guild is not None and not isinstance(self.guild, (PartialInviteGuild, Object)):
self.roles = [Role(state=self._state, guild=self.guild, data=role_data) for role_data in roles]
else:
self.roles = []
self.roles: List[Union[PartialInviteRole, Role, Object]] = self._resolve_roles(
data.get('roles', []) or data.get('role_ids', [])
)
@classmethod
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self:
@ -521,7 +635,12 @@ class Invite(Hashable):
# Upgrade the partial data if applicable
channel = guild.get_channel(channel.id) or channel
return cls(state=state, data=data, guild=guild, channel=channel)
return cls(
state=state,
data=data,
guild=guild,
channel=channel,
)
@classmethod
def from_gateway(cls, *, state: ConnectionState, data: GatewayInvitePayload) -> Self:
@ -534,14 +653,7 @@ class Invite(Hashable):
guild = state._get_or_create_unavailable_guild(guild_id) if guild_id is not None else None
channel = Object(id=channel_id)
res = cls(state=state, data=data, guild=guild, channel=channel) # type: ignore
# gateway events do not include role objects, only IDs
role_ids: list[Union[int, str]] = data.pop('role_ids', []) # type: ignore # .pop returns T | object
if role_ids and guild is not None and not isinstance(guild, (PartialInviteGuild, Object)):
res.roles = [guild.get_role(int(role_id)) or Object(role_id) for role_id in role_ids]
return res
return cls(state=state, data=data, guild=guild, channel=channel) # type: ignore
def _resolve_guild(
self,
@ -570,6 +682,32 @@ class Invite(Hashable):
return PartialInviteChannel(data)
def _resolve_roles(
self,
data: Optional[Sequence[Union[InviteRolePayload, int, str]]],
) -> list[Union[PartialInviteRole, Role, Object]]:
if not data:
return []
guild = self.guild
res: List[Union[PartialInviteRole, Role, Object]] = []
for role in data:
if isinstance(role, (int, str)):
role_id = int(role)
if guild is not None and not isinstance(guild, (PartialInviteGuild, Object)):
res.append(guild.get_role(role_id) or Object(role_id))
else:
res.append(Object(role_id))
else:
role_id = int(role['id'])
if guild is not None and not isinstance(guild, (PartialInviteGuild, Object)):
res.append(guild.get_role(role_id) or PartialInviteRole(self._state, role))
else:
res.append(PartialInviteRole(self._state, role))
return res
def __str__(self) -> str:
return self.url

4
discord/types/invite.py

@ -33,7 +33,7 @@ from .guild import InviteGuild, _GuildPreviewUnique
from .channel import PartialChannel
from .user import PartialUser
from .appinfo import PartialAppInfo
from .role import Role
from .role import InviteRole
InviteTargetType = Literal[1, 2]
InviteType = Literal[0, 1, 2]
@ -67,7 +67,7 @@ class Invite(IncompleteInvite, total=False):
type: InviteType
flags: NotRequired[int]
expires_at: Optional[str]
roles: NotRequired[list[Role]]
roles: NotRequired[list[InviteRole]]
class InviteWithCounts(Invite, _GuildPreviewUnique): ...

11
discord/types/role.py

@ -59,3 +59,14 @@ class RoleTags(TypedDict, total=False):
premium_subscriber: None
available_for_purchase: None
guild_connections: None
class InviteRole(TypedDict):
id: Snowflake
name: str
position: int
color: int
colors: RoleColours
icon: NotRequired[Optional[str]]
unicode_emoji: NotRequired[Optional[str]]
permissions: NotRequired[str]

8
docs/api.rst

@ -5518,6 +5518,14 @@ PartialInviteChannel
.. autoclass:: PartialInviteChannel()
:members:
PartialInviteRole
~~~~~~~~~~~~~~~~~~
.. attributetable:: PartialInviteRole
.. autoclass:: PartialInviteRole()
:members:
Invite
~~~~~~~

Loading…
Cancel
Save