Browse Source

Finish profile and application install param implementations, fix miscallenous related things

pull/10109/head
dolfies 3 years ago
parent
commit
8a315c48e6
  1. 124
      discord/appinfo.py
  2. 11
      discord/asset.py
  3. 34
      discord/member.py
  4. 148
      discord/profile.py
  5. 81
      discord/user.py
  6. 9
      docs/api.rst

124
discord/appinfo.py

@ -48,6 +48,7 @@ __all__ = (
'ApplicationBot',
'ApplicationCompany',
'ApplicationExecutable',
'ApplicationInstallParams',
'Application',
'PartialApplication',
'InteractionApplication',
@ -61,6 +62,24 @@ class ApplicationBot(User):
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two bots are equal.
.. describe:: x != y
Checks if two bots are not equal.
.. describe:: hash(x)
Return the bot's hash.
.. describe:: str(x)
Returns the bot's name with discriminator.
Attributes
-----------
application: :class:`Application`
@ -188,6 +207,12 @@ class ApplicationExecutable:
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the executable's name.
Attributes
-----------
name: :class:`str`
@ -213,6 +238,53 @@ class ApplicationExecutable:
self.launcher: bool = data['is_launcher']
self.application = application
def __repr__(self) -> str:
return f'<ApplicationExecutable name={self.name!r} os={self.os!r} launcher={self.launcher!r}>'
def __str__(self) -> str:
return self.name
class ApplicationInstallParams:
"""Represents an application's authorization parameters.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the authorization URL.
Attributes
----------
id: :class:`int`
The application's ID.
scopes: List[:class:`str`]
The list of `OAuth2 scopes <https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes>`_
to add the application with.
permissions: :class:`Permissions`
The permissions to grant to the added bot.
"""
__slots__ = ('id', 'scopes', 'permissions')
def __init__(self, id: int, data: dict):
self.id: int = id
self.scopes: List[str] = data.get('scopes', [])
self.permissions: Permissions = Permissions(int(data.get('permissions', 0)))
def __repr__(self) -> str:
return f'<ApplicationInstallParams id={self.id} scopes={self.scopes!r} permissions={self.permissions!r}>'
def __str__(self) -> str:
return self.url
@property
def url(self) -> str:
""":class:`str`: The URL to add the application with the parameters."""
return utils.oauth_url(self.id, permissions=self.permissions, scopes=self.scopes)
class PartialApplication(Hashable):
"""Represents a partial Application.
@ -280,6 +352,10 @@ class PartialApplication(Hashable):
A list of publishers that published the application. Only available for specific applications.
executables: List[:class:`ApplicationExecutable`]
A list of executables that are the application's. Only available for specific applications.
custom_install_url: Optional[:class:`str`]
The custom URL to use for authorizing the application, if specified.
install_params: Optional[:class:`ApplicationInstallParams`]
The parameters to use for authorizing the application, if specified.
"""
__slots__ = (
@ -302,13 +378,14 @@ class PartialApplication(Hashable):
'premium_tier_level',
'tags',
'max_participants',
'install_url',
'overlay',
'overlay_compatibility_hook',
'aliases',
'developers',
'publishers',
'executables',
'custom_install_url',
'install_params',
)
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
@ -351,15 +428,10 @@ class PartialApplication(Hashable):
self.overlay: bool = data.get('overlay', False)
self.overlay_compatibility_hook: bool = data.get('overlay_compatibility_hook', False)
install_params = data.get('install_params', {})
self.install_url = (
data.get('custom_install_url')
if not install_params
else utils.oauth_url(
self.id,
permissions=Permissions(int(install_params.get('permissions', 0))),
scopes=install_params.get('scopes', utils.MISSING),
)
params = data.get('install_params')
self.custom_install_url: Optional[str] = data.get('custom_install_url')
self.install_params: Optional[ApplicationInstallParams] = (
ApplicationInstallParams(self.id, params) if params else None
)
self.public: bool = data.get(
@ -401,12 +473,35 @@ class PartialApplication(Hashable):
""":class:`ApplicationFlags`: The flags of this application."""
return ApplicationFlags._from_value(self._flags)
@property
def install_url(self) -> Optional[str]:
""":class:`str`: The URL to install the application."""
return self.custom_install_url or self.install_params.url if self.install_params else None
class Application(PartialApplication):
"""Represents application info for an application you own.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two applications are equal.
.. describe:: x != y
Checks if two applications are not equal.
.. describe:: hash(x)
Return the application's hash.
.. describe:: str(x)
Returns the application's name.
Attributes
-------------
owner: :class:`abc.User`
@ -416,12 +511,9 @@ class Application(PartialApplication):
bot: Optional[:class:`ApplicationBot`]
The bot attached to the application, if any.
guild_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the guild to which it has been linked to.
The guild ID this application is linked to, if any.
primary_sku_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the id of the "Game SKU" that is created,
if it exists.
The application's primary SKU ID, if any.
slug: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the URL slug that links to the store page.
@ -678,6 +770,8 @@ class InteractionApplication(Hashable):
The application description.
type: Optional[:class:`ApplicationType`]
The type of application.
primary_sku_id: Optional[:class:`int`]
The application's primary SKU ID, if any.
"""
__slots__ = (

11
discord/asset.py

@ -235,6 +235,17 @@ class Asset(AssetMixin):
animated=animated,
)
@classmethod
def _from_guild_banner(cls, state: _State, guild_id: int, member_id: int, avatar: str) -> Self:
animated = avatar.startswith('a_')
format = 'gif' if animated else 'png'
return cls(
state,
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{avatar}.{format}?size=512",
key=avatar,
animated=animated,
)
@classmethod
def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self:
return cls(

34
discord/member.py

@ -215,9 +215,9 @@ class _ClientStatus:
def flatten_user(cls: Any) -> Type[Member]:
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
# Ignore private/special methods (or not)
# if attr.startswith('_'):
# continue
# Ignore private/special methods
if attr.startswith('_'):
continue
# Don't override what we already have
if attr in cls.__dict__:
@ -461,6 +461,16 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
# Signal to dispatch user_update
return to_return, u
def _get_voice_client_key(self) -> Tuple[int, str]:
return self._state.self_id, 'self_id' # type: ignore # self_id is always set at this point
def _get_voice_state_pair(self) -> Tuple[int, int]:
return self._state.self_id, self.dm_channel.id # type: ignore # self_id is always set at this point
async def _get_channel(self) -> DMChannel:
ch = await self.create_dm()
return ch
@property
def status(self) -> Status:
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
@ -733,6 +743,8 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
voice_channel: Optional[VocalGuildChannel] = MISSING,
timed_out_until: Optional[datetime.datetime] = MISSING,
avatar: Optional[bytes] = MISSING,
banner: Optional[bytes] = MISSING,
bio: Optional[str] = MISSING,
reason: Optional[str] = None,
) -> Optional[Member]:
"""|coro|
@ -798,6 +810,16 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
The member's new guild avatar. Pass ``None`` to remove the avatar.
You can only change your own guild avatar.
.. versionadded:: 2.0
banner: Optional[:class:`bytes`]
The member's new guild banner. Pass ``None`` to remove the banner.
You can only change your own guild banner.
.. versionadded:: 2.0
bio: Optional[:class:`str`]
The member's new guild "about me". Pass ``None`` to remove the bio.
You can only change your own guild bio.
.. versionadded:: 2.0
reason: Optional[:class:`str`]
The reason for editing this member. Shows up on the audit log.
@ -829,6 +851,12 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
if avatar is not MISSING:
payload['avatar'] = utils._bytes_to_base64_data(avatar) if avatar is not None else None
if banner is not MISSING:
payload['banner'] = utils._bytes_to_base64_data(banner) if banner is not None else None
if bio is not MISSING:
payload['bio'] = bio or ''
if me and payload:
data = await http.edit_me(self.guild.id, **payload)
payload = {}

148
discord/profile.py

@ -24,14 +24,18 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING
from typing import TYPE_CHECKING, List, Optional
from . import utils
from .appinfo import ApplicationInstallParams
from .asset import Asset
from .connections import PartialConnection
from .enums import PremiumType, try_enum
from .flags import ApplicationFlags
from .member import Member
from .mixins import Hashable
from .object import Object
from .permissions import Permissions
from .user import Note, User
from . import utils
if TYPE_CHECKING:
from datetime import datetime
@ -40,14 +44,13 @@ if TYPE_CHECKING:
from .state import ConnectionState
__all__ = (
'ApplicationProfile',
'UserProfile',
'MemberProfile',
)
class Profile:
"""Represents a Discord profile."""
if TYPE_CHECKING:
id: int
application_id: Optional[int]
@ -65,10 +68,17 @@ class Profile:
super().__init__(**kwargs)
self._flags: int = user.pop('flags', 0)
self.bio: Optional[str] = user.pop('bio') or None
self.note: Note = Note(kwargs['state'], self.id, user=self)
self.bio: Optional[str] = user.pop('bio', None) or None
self.note: Note = Note(kwargs['state'], self.id, user=getattr(self, '_user', self)) # type: ignore
# We need to do a bit of a hack here because premium_since is massively overloaded
guild_premium_since = getattr(self, 'premium_since', utils.MISSING)
if guild_premium_since is not utils.MISSING:
self.guild_premium_since = guild_premium_since
self.premium_type: Optional[PremiumType] = (
try_enum(PremiumType, user.pop('premium_type')) if user.get('premium_type') else None
)
self.premium_since: Optional[datetime] = utils.parse_time(data['premium_since'])
self.boosting_since: Optional[datetime] = utils.parse_time(data['premium_guild_since'])
self.connections: List[PartialConnection] = [PartialConnection(d) for d in data['connected_accounts']]
@ -77,9 +87,7 @@ class Profile:
self.mutual_friends: Optional[List[User]] = self._parse_mutual_friends(data.get('mutual_friends'))
application = data.get('application', {})
install_params = application.get('install_params', {})
self.application_id = app_id = utils._get_as_snowflake(application, 'id')
self.install_url = application.get('custom_install_url') if not install_params else utils.oauth_url(app_id, permissions=Permissions(int(install_params.get('permissions', 0))), scopes=install_params.get('scopes', utils.MISSING)) # type: ignore # app_id is always present here
self.application: Optional[ApplicationProfile] = ApplicationProfile(data=application) if application else None
def _parse_mutual_guilds(self, mutual_guilds) -> Optional[List[Guild]]:
if mutual_guilds is None:
@ -90,7 +98,7 @@ class Profile:
def get_guild(guild):
return state._get_guild(int(guild['id'])) or Object(id=int(guild['id']))
return list(filter(None, map(get_guild, mutual_guilds))) # type: ignore # Lying for better developer UX
return list(map(get_guild, mutual_guilds)) # type: ignore # Lying for better developer UX
def _parse_mutual_friends(self, mutual_friends) -> Optional[List[User]]:
if mutual_friends is None:
@ -105,17 +113,79 @@ class Profile:
return self.premium_since is not None
class ApplicationProfile(Hashable):
"""Represents a Discord application profile.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two applications are equal.
.. describe:: x != y
Checks if two applications are not equal.
.. describe:: hash(x)
Return the applications's hash.
Attributes
------------
id: :class:`int`
The application's ID.
verified: :class:`bool`
Indicates if the application is verified.
popular_application_command_ids: List[:class:`int`]
A list of the IDs of the application's popular commands.
primary_sku_id: Optional[:class:`int`]
The application's primary SKU ID, if any.
custom_install_url: Optional[:class:`str`]
The custom URL to use for authorizing the application, if specified.
install_params: Optional[:class:`ApplicationInstallParams`]
The parameters to use for authorizing the application, if specified.
"""
def __init__(self, data: dict) -> None:
self.id: int = int(data['id'])
self.verified: bool = data.get('verified', False)
self.popular_application_command_ids: List[int] = [int(id) for id in data.get('popular_application_command_ids', [])]
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, 'primary_sku_id')
self._flags: int = data.get('flags', 0)
params = data.get('install_params')
self.custom_install_url: Optional[str] = data.get('custom_install_url')
self.install_params: Optional[ApplicationInstallParams] = (
ApplicationInstallParams(self.id, params) if params else None
)
def __repr__(self) -> str:
return f'<ApplicationProfile id={self.id} verified={self.verified}>'
@property
def flags(self) -> ApplicationFlags:
""":class:`ApplicationFlags`: The flags of this application."""
return ApplicationFlags._from_value(self._flags)
@property
def install_url(self) -> Optional[str]:
""":class:`str`: The URL to install the application."""
return self.custom_install_url or self.install_params.url if self.install_params else None
class UserProfile(Profile, User):
"""Represents a Discord user's profile. This is a :class:`User` with extended attributes.
Attributes
-----------
application_id: Optional[:class:`int`]
The ID of the application that this user is attached to, if applicable.
install_url: Optional[:class:`str`]
The URL to invite the application to your guild with.
application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if a bot.
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
premium_type: Optional[:class:`PremiumType`]
Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). Could be None if the user is not premium.
premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies how long a user has been premium (had Nitro).
``None`` if the user is not a premium user.
@ -142,17 +212,28 @@ class MemberProfile(Profile, Member):
Attributes
-----------
application_id: Optional[:class:`int`]
The ID of the application that this user is attached to, if applicable.
install_url: Optional[:class:`str`]
The URL to invite the application to your guild with.
application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if a bot.
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
guild_bio: Optional[:class:`str`]
The user's "about me" field for the guild. Could be ``None``.
guild_premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies the date and time in UTC when the member used their
"Nitro boost" on the guild, if available. This could be ``None``.
.. note::
This is renamed from :attr:`Member.premium_since` because of name collisions.
premium_type: Optional[:class:`PremiumType`]
Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). Could be None if the user is not premium.
premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies how long a user has been premium (had Nitro).
``None`` if the user is not a premium user.
.. note::
This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`
boosting_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first boosted a guild.
An aware datetime object that specifies when a user first boosted any guild.
connections: Optional[List[:class:`PartialConnection`]]
The connected accounts that show up on the profile.
note: :class:`Note`
@ -165,8 +246,33 @@ class MemberProfile(Profile, Member):
``None`` if you didn't fetch mutuals.
"""
def __init__(self, *, state: ConnectionState, data: dict, guild: Guild):
super().__init__(state=state, guild=guild, data=data)
member = data['guild_member']
self._banner: Optional[str] = member.get('banner')
self.guild_bio: Optional[str] = member.get('bio') or None
def __repr__(self) -> str:
return (
f'<MemberProfile id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}'
f' bot={self._user.bot} nick={self.nick!r} premium={self.premium} guild={self.guild!r}>'
)
@property
def display_banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the member's display banner.
For regular members this is just their banner (if available), but
if they have a guild specific banner then that
is returned instead.
"""
return self.guild_banner or self._user.banner
@property
def guild_banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner
the member has. If unavailable, ``None`` is returned.
"""
if self._banner is None:
return None
return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner)

81
discord/user.py

@ -75,6 +75,8 @@ __all__ = (
class Note:
"""Represents a Discord note.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
@ -95,22 +97,26 @@ class Note:
.. describe:: len(x)
Returns the note's length.
Attributes
-----------
user_id: :class:`int`
The user ID the note is for.
"""
__slots__ = ('_state', '_note', '_user_id', '_user')
__slots__ = ('_state', '_note', 'user_id', '_user')
def __init__(
self, state: ConnectionState, user_id: int, *, user: _Snowflake = MISSING, note: Optional[str] = MISSING
self, state: ConnectionState, user_id: int, *, user: Optional[User] = None, note: Optional[str] = MISSING
) -> None:
self._state = state
self._user_id = user_id
self._note = note
if user is not MISSING:
self._user = user
self._note: Optional[str] = note
self.user_id: int = user_id
self._user: Optional[User] = user
@property
def note(self) -> Optional[str]:
"""Returns the note.
"""Optional[:class:`str`]: Returns the note.
There is an alias for this called :attr:`value`.
@ -125,7 +131,7 @@ class Note:
@property
def value(self) -> Optional[str]:
"""Returns the note.
"""Optional[:class:`str`]: Returns the note.
This is an alias of :attr:`note`.
@ -136,15 +142,10 @@ class Note:
"""
return self.note
@cached_slot_property('_user')
def user(self) -> _Snowflake:
""":class:`~abc.Snowflake`: Returns the :class:`User` or :class:`Object` the note belongs to."""
user_id = self._user_id
user = self._state.get_user(user_id)
if user is None:
user = Object(user_id)
return user
@property
def user(self) -> Optional[User]:
"""Optional[:class:`User`]: Returns the :class:`User` the note belongs to."""
return self._user or self._state.get_user(self.user_id)
async def fetch(self) -> Optional[str]:
"""|coro|
@ -162,7 +163,7 @@ class Note:
The note or ``None`` if it doesn't exist.
"""
try:
data = await self._state.http.get_note(self.user.id)
data = await self._state.http.get_note(self.user_id)
self._note = data['note']
return data['note']
except NotFound: # 404 = no note
@ -179,7 +180,7 @@ class Note:
HTTPException
Changing the note failed.
"""
await self._state.http.set_note(self._user_id, note=note)
await self._state.http.set_note(self.user_id, note=note)
self._note = note
async def delete(self) -> None:
@ -206,32 +207,25 @@ class Note:
note = self._note
if note is MISSING:
raise ClientException('Note is not fetched')
elif note is None:
return ''
else:
return note
return note or ''
def __bool__(self) -> bool:
try:
return bool(self._note)
except TypeError:
return False
return bool(str(self))
def __eq__(self, other: object) -> bool:
return isinstance(other, Note) and self._note == other._note and self._user_id == other._user_id
return isinstance(other, Note) and self._note == other._note and self.user_id == other.user_id
def __ne__(self, other: object) -> bool:
if isinstance(other, Note):
return self._note != other._note or self._user_id != other._user_id
return self._note != other._note or self.user_id != other.user_id
return True
def __hash__(self) -> int:
return hash((self._note, self._user_id))
return hash((self._note, self.user_id))
def __len__(self) -> int:
if note := self._note:
return len(note)
return 0
note = str(self)
return len(note) if note else 0
class _UserTag:
@ -367,7 +361,6 @@ class BaseUser(_UserTag):
.. versionadded:: 2.0
.. note::
This information is only available via :meth:`Client.fetch_user`.
"""
@ -375,6 +368,16 @@ class BaseUser(_UserTag):
return None
return Asset._from_user_banner(self._state, self.id, self._banner)
@property
def display_banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available.
This is the same as :attr:`banner` and is here for compatibility.
.. versionadded:: 2.0
"""
return self.banner
@property
def accent_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: Returns the user's accent colour, if applicable.
@ -518,7 +521,7 @@ class ClientUser(BaseUser):
mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working.
premium_type: Optional[:class:`PremiumType`]
Specifies the type of premium a user has (i.e. Nitro or Nitro Classic). Could be None if the user is not premium.
Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). Could be None if the user is not premium.
note: :class:`Note`
The user's note. Not pre-fetched.
@ -579,7 +582,7 @@ class ClientUser(BaseUser):
self._premium_usage_flags = data.get('premium_usage_flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False)
self.premium_type = try_enum(PremiumType, data['premium_type']) if 'premium_type' in data else None
self.bio = data.get('bio')
self.bio = data.get('bio') or None
self.nsfw_allowed = data.get('nsfw_allowed')
def get_relationship(self, user_id: int) -> Optional[Relationship]:
@ -721,8 +724,8 @@ class ClientUser(BaseUser):
accent_colour/_color: :class:`Colour`
A :class:`Colour` object of the colour you want to set your profile to.
bio: :class:`str`
Your 'about me' section.
Could be ``None`` to represent no 'about me'.
Your "about me" section.
Could be ``None`` to represent no bio.
date_of_birth: :class:`datetime.datetime`
Your date of birth. Can only ever be set once.
@ -743,7 +746,7 @@ class ClientUser(BaseUser):
"""
args: Dict[str, Any] = {}
if any(x is not MISSING for x in ('new_password', 'email', 'username', 'discriminator')):
if any(x is not MISSING for x in (new_password, email, username, discriminator)):
if password is MISSING:
raise ValueError('Password is required')
args['password'] = password

9
docs/api.rst

@ -4266,6 +4266,11 @@ Application
.. autoclass:: PartialApplication()
:members:
.. attributetable:: InteractionApplication
.. autoclass:: InteractionApplication()
:members:
.. attributetable:: ApplicationCompany
.. autoclass:: ApplicationCompany()
@ -4276,9 +4281,9 @@ Application
.. autoclass:: ApplicationExecutable()
:members:
.. attributetable:: InteractionApplication
.. attributetable:: ApplicationInstallParams
.. autoclass:: InteractionApplication()
.. autoclass:: ApplicationInstallParams()
:members:
Team

Loading…
Cancel
Save