Browse Source

Rework profiles and notes, add note_update event

pull/10109/head
dolfies 2 years ago
parent
commit
dd733e931b
  1. 3
      discord/application.py
  2. 53
      discord/client.py
  3. 56
      discord/guild.py
  4. 23
      discord/http.py
  5. 52
      discord/member.py
  6. 198
      discord/profile.py
  7. 16
      discord/state.py
  8. 31
      discord/types/application.py
  9. 5
      discord/types/gateway.py
  10. 19
      discord/types/member.py
  11. 73
      discord/types/profile.py
  12. 15
      discord/types/user.py
  13. 114
      discord/user.py
  14. 15
      docs/api.rst

3
discord/application.py

@ -83,6 +83,7 @@ if TYPE_CHECKING:
Achievement as AchievementPayload, Achievement as AchievementPayload,
ActivityStatistics as ActivityStatisticsPayload, ActivityStatistics as ActivityStatisticsPayload,
Application as ApplicationPayload, Application as ApplicationPayload,
ApplicationInstallParams as ApplicationInstallParamsPayload,
Asset as AssetPayload, Asset as AssetPayload,
BaseApplication as BaseApplicationPayload, BaseApplication as BaseApplicationPayload,
Branch as BranchPayload, Branch as BranchPayload,
@ -787,7 +788,7 @@ class ApplicationInstallParams:
self.permissions: Permissions = permissions or Permissions(0) self.permissions: Permissions = permissions or Permissions(0)
@classmethod @classmethod
def from_application(cls, application: Snowflake, data: dict) -> ApplicationInstallParams: def from_application(cls, application: Snowflake, data: ApplicationInstallParamsPayload) -> ApplicationInstallParams:
return cls( return cls(
application.id, application.id,
scopes=data.get('scopes', []), scopes=data.get('scopes', []),

53
discord/client.py

@ -2162,13 +2162,20 @@ class Client:
return User(state=self._connection, data=data) return User(state=self._connection, data=data)
async def fetch_user_profile( async def fetch_user_profile(
self, user_id: int, /, *, with_mutuals: bool = True, fetch_note: bool = True self,
user_id: int,
/,
*,
with_mutual_guilds: bool = True,
with_mutual_friends_count: bool = False,
with_mutual_friends: bool = True,
) -> UserProfile: ) -> UserProfile:
"""|coro| """|coro|
Gets an arbitrary user's profile. Retrieves a :class:`.UserProfile` based on their user ID.
You must share a guild or be friends with this user to You must share a guild, be friends with this user,
or have an incoming friend request from them to
get this information (unless the user is a bot). get this information (unless the user is a bot).
.. versionchanged:: 2.0 .. versionchanged:: 2.0
@ -2179,11 +2186,22 @@ class Client:
------------ ------------
user_id: :class:`int` user_id: :class:`int`
The ID of the user to fetch their profile for. The ID of the user to fetch their profile for.
with_mutuals: :class:`bool` with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds and friends. Whether to fetch mutual guilds.
This fills in :attr:`.UserProfile.mutual_guilds` & :attr:`.UserProfile.mutual_friends`. This fills in :attr:`.UserProfile.mutual_guilds`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note. .. versionadded:: 2.0
with_mutual_friends_count: :class:`bool`
Whether to fetch the number of mutual friends.
This fills in :attr:`.UserProfile.mutual_friends_count`.
.. versionadded:: 2.0
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`.UserProfile.mutual_friends` and :attr:`.UserProfile.mutual_friends_count`,
but requires an extra API call.
.. versionadded:: 2.0
Raises Raises
------- -------
@ -2200,19 +2218,14 @@ class Client:
The profile of the user. The profile of the user.
""" """
state = self._connection state = self._connection
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals) data = await state.http.get_user_profile(
user_id, with_mutual_guilds=with_mutual_guilds, with_mutual_friends_count=with_mutual_friends_count
if with_mutuals: )
if not data['user'].get('bot', False): mutual_friends = None
data['mutual_friends'] = await state.http.get_mutual_friends(user_id) if with_mutual_friends and not data['user'].get('bot', False):
else: mutual_friends = await state.http.get_mutual_friends(user_id)
data['mutual_friends'] = []
profile = UserProfile(state=state, data=data)
if fetch_note:
await profile.note.fetch()
return profile return UserProfile(state=state, data=data, mutual_friends=mutual_friends)
async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]: async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]:
"""|coro| """|coro|

56
discord/guild.py

@ -2333,32 +2333,45 @@ class Guild(Hashable):
return Member(data=data, state=self._state, guild=self) return Member(data=data, state=self._state, guild=self)
async def fetch_member_profile( async def fetch_member_profile(
self, member_id: int, /, *, with_mutuals: bool = True, fetch_note: bool = True self,
member_id: int,
/,
*,
with_mutual_guilds: bool = True,
with_mutual_friends_count: bool = False,
with_mutual_friends: bool = True,
) -> MemberProfile: ) -> MemberProfile:
"""|coro| """|coro|
Gets an arbitrary member's profile. Retrieves a :class:`.MemberProfile` from a guild ID, and a member ID.
.. versionadded:: 2.0
Parameters Parameters
------------ ------------
member_id: :class:`int` member_id: :class:`int`
The ID of the member to fetch their profile for. The ID of the member to fetch their profile for.
with_mutuals: :class:`bool` with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds and friends. Whether to fetch mutual guilds.
This fills in :attr:`.MemberProfile.mutual_guilds` & :attr:`.MemberProfile.mutual_friends`. This fills in :attr:`.MemberProfile.mutual_guilds`.
fetch_note: :class:`bool` with_mutual_friends_count: :class:`bool`
Whether to pre-fetch the user's note. Whether to fetch the number of mutual friends.
This fills in :attr:`.MemberProfile.mutual_friends_count`.
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`.MemberProfile.mutual_friends` and :attr:`.MemberProfile.mutual_friends_count`,
but requires an extra API call.
Raises Raises
------- -------
NotFound NotFound
A user with this ID does not exist. A user with this ID does not exist.
Forbidden Forbidden
Not allowed to fetch this profile. You do not have a mutual with this user, and and the user is not a bot.
HTTPException HTTPException
Fetching the profile failed. Fetching the profile failed.
InvalidData InvalidData
The member is not in this guild. The member is not in this guild or has blocked you.
Returns Returns
-------- --------
@ -2366,23 +2379,18 @@ class Guild(Hashable):
The profile of the member. The profile of the member.
""" """
state = self._state state = self._state
data = await state.http.get_user_profile(member_id, self.id, with_mutual_guilds=with_mutuals) data = await state.http.get_user_profile(
member_id, self.id, with_mutual_guilds=with_mutual_guilds, with_mutual_friends_count=with_mutual_friends_count
)
if 'guild_member_profile' not in data:
raise InvalidData('Member is not in this guild')
if 'guild_member' not in data: if 'guild_member' not in data:
raise InvalidData('Member not in this guild') raise InvalidData('Member has blocked you')
mutual_friends = None
if with_mutuals: if with_mutual_friends and not data['user'].get('bot', False):
if not data['user'].get('bot', False): mutual_friends = await state.http.get_mutual_friends(member_id)
data['mutual_friends'] = await state.http.get_mutual_friends(member_id)
else:
data['mutual_friends'] = []
profile = MemberProfile(state=state, data=data, guild=self)
if fetch_note:
await profile.note.fetch()
return profile return MemberProfile(state=state, data=data, mutual_friends=mutual_friends, guild=self)
async def fetch_ban(self, user: Snowflake) -> BanEntry: async def fetch_ban(self, user: Snowflake) -> BanEntry:
"""|coro| """|coro|

23
discord/http.py

@ -97,6 +97,7 @@ if TYPE_CHECKING:
member, member,
message, message,
payments, payments,
profile,
promotions, promotions,
template, template,
role, role,
@ -4069,14 +4070,22 @@ class HTTPClient:
value = '{0}?encoding={1}&v={2}' value = '{0}?encoding={1}&v={2}'
return value.format(data['url'], encoding, INTERNAL_API_VERSION) return value.format(data['url'], encoding, INTERNAL_API_VERSION)
def get_user(self, user_id: Snowflake) -> Response[user.User]: def get_user(self, user_id: Snowflake) -> Response[user.APIUser]:
return self.request(Route('GET', '/users/{user_id}', user_id=user_id)) return self.request(Route('GET', '/users/{user_id}', user_id=user_id))
def get_user_profile( def get_user_profile(
self, user_id: Snowflake, guild_id: Snowflake = MISSING, *, with_mutual_guilds: bool = True self,
): # TODO: return type user_id: Snowflake,
params: Dict[str, Any] = {'with_mutual_guilds': str(with_mutual_guilds).lower()} guild_id: Optional[Snowflake] = None,
if guild_id is not MISSING: *,
with_mutual_guilds: bool = True,
with_mutual_friends_count: bool = False,
) -> Response[profile.Profile]:
params: Dict[str, Any] = {
'with_mutual_guilds': str(with_mutual_guilds).lower(),
'with_mutual_friends_count': str(with_mutual_friends_count).lower(),
}
if guild_id:
params['guild_id'] = guild_id params['guild_id'] = guild_id
return self.request(Route('GET', '/users/{user_id}/profile', user_id=user_id), params=params) return self.request(Route('GET', '/users/{user_id}/profile', user_id=user_id), params=params)
@ -4084,10 +4093,10 @@ class HTTPClient:
def get_mutual_friends(self, user_id: Snowflake): # TODO: return type def get_mutual_friends(self, user_id: Snowflake): # TODO: return type
return self.request(Route('GET', '/users/{user_id}/relationships', user_id=user_id)) return self.request(Route('GET', '/users/{user_id}/relationships', user_id=user_id))
def get_notes(self): # TODO: return type def get_notes(self) -> Response[Dict[Snowflake, str]]:
return self.request(Route('GET', '/users/@me/notes')) return self.request(Route('GET', '/users/@me/notes'))
def get_note(self, user_id: Snowflake): # TODO: return type def get_note(self, user_id: Snowflake) -> Response[user.Note]:
return self.request(Route('GET', '/users/@me/notes/{user_id}', user_id=user_id)) return self.request(Route('GET', '/users/@me/notes/{user_id}', user_id=user_id))
def set_note(self, user_id: Snowflake, *, note: Optional[str] = None) -> Response[None]: def set_note(self, user_id: Snowflake, *, note: Optional[str] = None) -> Response[None]:

52
discord/member.py

@ -58,6 +58,7 @@ if TYPE_CHECKING:
from .channel import DMChannel, VoiceChannel, StageChannel, GroupChannel from .channel import DMChannel, VoiceChannel, StageChannel, GroupChannel
from .flags import PublicUserFlags from .flags import PublicUserFlags
from .guild import Guild from .guild import Guild
from .profile import MemberProfile
from .types.activity import ( from .types.activity import (
PartialPresenceUpdate, PartialPresenceUpdate,
) )
@ -1107,3 +1108,54 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
Sending the friend request failed. Sending the friend request failed.
""" """
await self._state.http.add_relationship(self._user.id, action=RelationshipAction.send_friend_request) await self._state.http.add_relationship(self._user.id, action=RelationshipAction.send_friend_request)
async def profile(
self,
*,
with_mutual_guilds: bool = True,
with_mutual_friends_count: bool = False,
with_mutual_friends: bool = True,
) -> MemberProfile:
"""|coro|
A shorthand method to retrieve a :class:`MemberProfile` for the member.
Parameters
------------
with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds.
This fills in :attr:`MemberProfile.mutual_guilds`.
.. versionadded:: 2.0
with_mutual_friends_count: :class:`bool`
Whether to fetch the number of mutual friends.
This fills in :attr:`MemberProfile.mutual_friends_count`.
.. versionadded:: 2.0
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`MemberProfile.mutual_friends` and :attr:`MemberProfile.mutual_friends_count`,
but requires an extra API call.
.. versionadded:: 2.0
Raises
-------
Forbidden
Not allowed to fetch this profile.
HTTPException
Fetching the profile failed.
InvalidData
The member is not in this guild or has blocked you.
Returns
--------
:class:`MemberProfile`
The profile of the member.
"""
return await self.guild.fetch_member_profile(
self._user.id,
with_mutual_guilds=with_mutual_guilds,
with_mutual_friends_count=with_mutual_friends_count,
with_mutual_friends=with_mutual_friends,
)

198
discord/profile.py

@ -34,16 +34,23 @@ from .enums import PremiumType, try_enum
from .flags import ApplicationFlags from .flags import ApplicationFlags
from .member import Member from .member import Member
from .mixins import Hashable from .mixins import Hashable
from .user import Note, User from .user import User
if TYPE_CHECKING: if TYPE_CHECKING:
from datetime import datetime from datetime import datetime
from .guild import Guild from .guild import Guild
from .state import ConnectionState from .state import ConnectionState
from .types.profile import (
Profile as ProfilePayload,
ProfileApplication as ProfileApplicationPayload,
MutualGuild as MutualGuildPayload,
)
from .types.user import PartialUser as PartialUserPayload
__all__ = ( __all__ = (
'ApplicationProfile', 'ApplicationProfile',
'MutualGuild',
'UserProfile', 'UserProfile',
'MemberProfile', 'MemberProfile',
) )
@ -52,23 +59,29 @@ __all__ = (
class Profile: class Profile:
if TYPE_CHECKING: if TYPE_CHECKING:
id: int id: int
application_id: Optional[int] bot: bool
_state: ConnectionState _state: ConnectionState
def __init__(self, **kwargs) -> None: # TODO: type data def __init__(self, **kwargs) -> None:
data = kwargs.pop('data') data: ProfilePayload = kwargs.pop('data')
user = data['user'] user = data['user']
mutual_friends: List[PartialUserPayload] = kwargs.pop('mutual_friends', None)
if (member := data.get('guild_member')) is not None: member = data.get('guild_member')
if member is not None:
member['user'] = user member['user'] = user
kwargs['data'] = member kwargs['data'] = member
else: else:
kwargs['data'] = user kwargs['data'] = user
# n.b. this class is subclassed by UserProfile and MemberProfile
# which subclass either User or Member respectively
# Because of this, we call super().__init__ here
# after ensuring the data kwarg is set correctly
super().__init__(**kwargs) super().__init__(**kwargs)
state = self._state
self.bio: Optional[str] = user.pop('bio', None) or None self.bio: Optional[str] = user['bio'] 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 # 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) guild_premium_since = getattr(self, 'premium_since', utils.MISSING)
@ -76,36 +89,42 @@ class Profile:
self.guild_premium_since = guild_premium_since self.guild_premium_since = guild_premium_since
self.premium_type: Optional[PremiumType] = ( self.premium_type: Optional[PremiumType] = (
try_enum(PremiumType, user.pop('premium_type')) if user.get('premium_type') else None try_enum(PremiumType, data['premium_type']) if user.get('premium_type') else None
) )
self.premium_since: Optional[datetime] = utils.parse_time(data['premium_since']) self.premium_since: Optional[datetime] = utils.parse_time(data['premium_since'])
self.boosting_since: Optional[datetime] = utils.parse_time(data['premium_guild_since']) self.premium_guild_since: Optional[datetime] = utils.parse_time(data['premium_guild_since'])
self.connections: List[PartialConnection] = [PartialConnection(d) for d in data['connected_accounts']] self.connections: List[PartialConnection] = [PartialConnection(d) for d in data['connected_accounts']]
self.mutual_guilds: Optional[List[Guild]] = self._parse_mutual_guilds(data.get('mutual_guilds')) self.mutual_guilds: Optional[List[MutualGuild]] = (
self.mutual_friends: Optional[List[User]] = self._parse_mutual_friends(data.get('mutual_friends')) [MutualGuild(state=state, data=d) for d in data['mutual_guilds']] if 'mutual_guilds' in data else None
)
self.mutual_friends: Optional[List[User]] = self._parse_mutual_friends(mutual_friends)
self._mutual_friends_count: Optional[int] = data.get('mutual_friends_count')
application = data.get('application', {}) application = data.get('application')
self.application: Optional[ApplicationProfile] = ApplicationProfile(data=application) if application else None self.application: Optional[ApplicationProfile] = ApplicationProfile(data=application) if application else None
def _parse_mutual_guilds(self, mutual_guilds) -> Optional[List[Guild]]: def _parse_mutual_friends(self, mutual_friends: List[PartialUserPayload]) -> Optional[List[User]]:
if mutual_guilds is None: if self.bot:
return # Bots don't have friends
return []
state = self._state
def get_guild(guild):
return state._get_or_create_unavailable_guild(int(guild['id']))
return list(map(get_guild, mutual_guilds))
def _parse_mutual_friends(self, mutual_friends) -> Optional[List[User]]:
if mutual_friends is None: if mutual_friends is None:
return return
state = self._state state = self._state
return [state.store_user(friend) for friend in mutual_friends] return [state.store_user(friend) for friend in mutual_friends]
@property
def mutual_friends_count(self) -> Optional[int]:
"""Optional[:class:`int`]: The number of mutual friends the user has with the client user."""
if self.bot:
# Bots don't have friends
return 0
if self._mutual_friends_count is not None:
return self._mutual_friends_count
if self.mutual_friends is not None:
return len(self.mutual_friends)
@property @property
def premium(self) -> bool: def premium(self) -> bool:
""":class:`bool`: Indicates if the user is a premium user.""" """:class:`bool`: Indicates if the user is a premium user."""
@ -148,7 +167,17 @@ class ApplicationProfile(Hashable):
The parameters to use for authorizing the application, if specified. The parameters to use for authorizing the application, if specified.
""" """
def __init__(self, data: dict) -> None: __slots__ = (
'id',
'verified',
'popular_application_command_ids',
'primary_sku_id',
'_flags',
'custom_install_url',
'install_params',
)
def __init__(self, data: ProfileApplicationPayload) -> None:
self.id: int = int(data['id']) self.id: int = int(data['id'])
self.verified: bool = data.get('verified', False) 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.popular_application_command_ids: List[int] = [int(id) for id in data.get('popular_application_command_ids', [])]
@ -181,6 +210,49 @@ class ApplicationProfile(Hashable):
return f'https://discord.com/store/skus/{self.primary_sku_id}/unknown' return f'https://discord.com/store/skus/{self.primary_sku_id}/unknown'
class MutualGuild(Hashable):
"""Represents a mutual guild between a user and the client user.
.. container:: operations
.. describe:: x == y
Checks if two guilds are equal.
.. describe:: x != y
Checks if two guilds are not equal.
.. describe:: hash(x)
Returns the guild's hash.
.. versionadded:: 2.0
Attributes
------------
id: :class:`int`
The guild's ID.
nick: Optional[:class:`str`]
The guild specific nickname of the user.
"""
__slots__ = ('id', 'nick', '_state')
def __init__(self, *, state: ConnectionState, data: MutualGuildPayload) -> None:
self._state = state
self.id: int = int(data['id'])
self.nick: Optional[str] = data.get('nick')
def __repr__(self) -> str:
return f'<MutualGuild guild={self.guild!r} nick={self.nick!r}>'
@property
def guild(self) -> Guild:
""":class:`Guild`: The guild that the user is mutual with."""
return self._state._get_or_create_unavailable_guild(self.id)
class UserProfile(Profile, User): class UserProfile(Profile, User):
"""Represents a Discord user's profile. """Represents a Discord user's profile.
@ -209,7 +281,7 @@ class UserProfile(Profile, User):
Attributes Attributes
----------- -----------
application: Optional[:class:`ApplicationProfile`] application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if a bot. The application profile of the user, if it is a bot.
bio: Optional[:class:`str`] bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``. The user's "about me" field. Could be ``None``.
premium_type: Optional[:class:`PremiumType`] premium_type: Optional[:class:`PremiumType`]
@ -217,23 +289,41 @@ class UserProfile(Profile, User):
premium_since: Optional[:class:`datetime.datetime`] premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies how long a user has been premium (had Nitro). An aware datetime object that specifies how long a user has been premium (had Nitro).
``None`` if the user is not a premium user. ``None`` if the user is not a premium user.
boosting_since: Optional[:class:`datetime.datetime`] premium_guild_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 Nitro boosted a guild.
connections: Optional[List[:class:`PartialConnection`]] connections: Optional[List[:class:`PartialConnection`]]
The connected accounts that show up on the profile. The connected accounts that show up on the profile.
note: :class:`Note` mutual_guilds: Optional[List[:class:`MutualGuild`]]
Represents the note on the profile.
mutual_guilds: Optional[List[:class:`Guild`]]
A list of guilds that you share with the user. A list of guilds that you share with the user.
``None`` if you didn't fetch mutuals. ``None`` if you didn't fetch mutual guilds.
mutual_friends: Optional[List[:class:`User`]] mutual_friends: Optional[List[:class:`User`]]
A list of friends that you share with the user. A list of friends that you share with the user.
``None`` if you didn't fetch mutuals. ``None`` if you didn't fetch mutual friends.
""" """
__slots__ = (
'bio',
'premium_type',
'premium_since',
'premium_guild_since',
'connections',
'mutual_guilds',
'mutual_friends',
'_mutual_friends_count',
'application',
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<UserProfile id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot} system={self.system} premium={self.premium}>' return f'<UserProfile id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot} system={self.system} premium={self.premium}>'
@property
def display_bio(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the user's display bio.
This is the same as :attr:`bio` and is here for compatibility.
"""
return self.bio
class MemberProfile(Profile, Member): class MemberProfile(Profile, Member):
"""Represents a Discord member's profile. """Represents a Discord member's profile.
@ -265,7 +355,7 @@ class MemberProfile(Profile, Member):
Attributes Attributes
----------- -----------
application: Optional[:class:`ApplicationProfile`] application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if a bot. The application profile of the user, if it is a bot.
bio: Optional[:class:`str`] bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``. The user's "about me" field. Could be ``None``.
guild_bio: Optional[:class:`str`] guild_bio: Optional[:class:`str`]
@ -275,6 +365,7 @@ class MemberProfile(Profile, Member):
"Nitro boost" on the guild, if available. This could be ``None``. "Nitro boost" on the guild, if available. This could be ``None``.
.. note:: .. note::
This is renamed from :attr:`Member.premium_since` because of name collisions. This is renamed from :attr:`Member.premium_since` because of name collisions.
premium_type: Optional[:class:`PremiumType`] 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. 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.
@ -283,14 +374,13 @@ class MemberProfile(Profile, Member):
``None`` if the user is not a premium user. ``None`` if the user is not a premium user.
.. note:: .. note::
This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`. This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`.
boosting_since: Optional[:class:`datetime.datetime`] premium_guild_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first boosted any guild. An aware datetime object that specifies when a user first Nitro boosted a guild.
connections: Optional[List[:class:`PartialConnection`]] connections: Optional[List[:class:`PartialConnection`]]
The connected accounts that show up on the profile. The connected accounts that show up on the profile.
note: :class:`Note` mutual_guilds: Optional[List[:class:`MutualGuild`]]
Represents the note on the profile.
mutual_guilds: Optional[List[:class:`Guild`]]
A list of guilds that you share with the user. A list of guilds that you share with the user.
``None`` if you didn't fetch mutuals. ``None`` if you didn't fetch mutuals.
mutual_friends: Optional[List[:class:`User`]] mutual_friends: Optional[List[:class:`User`]]
@ -298,8 +388,24 @@ class MemberProfile(Profile, Member):
``None`` if you didn't fetch mutuals. ``None`` if you didn't fetch mutuals.
""" """
def __init__(self, *, state: ConnectionState, data: dict, guild: Guild): __slots__ = (
super().__init__(state=state, guild=guild, data=data) 'bio',
'guild_premium_since',
'premium_type',
'premium_since',
'premium_guild_since',
'connections',
'mutual_guilds',
'mutual_friends',
'_mutual_friends_count',
'application',
'_banner',
'guild_bio',
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
data = kwargs['data']
member = data['guild_member'] member = data['guild_member']
self._banner: Optional[str] = member.get('banner') self._banner: Optional[str] = member.get('banner')
self.guild_bio: Optional[str] = member.get('bio') or None self.guild_bio: Optional[str] = member.get('bio') or None
@ -328,3 +434,13 @@ class MemberProfile(Profile, Member):
if self._banner is None: if self._banner is None:
return None return None
return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner)
@property
def display_bio(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the member's display bio.
For regular members this is just their bio (if available), but
if they have a guild specific bio then that
is returned instead.
"""
return self.guild_bio or self.bio

16
discord/state.py

@ -54,7 +54,7 @@ from discord_protos import UserSettingsType
from .errors import ClientException, InvalidData, NotFound from .errors import ClientException, InvalidData, NotFound
from .guild import ApplicationCommandCounts, Guild from .guild import ApplicationCommandCounts, Guild
from .activity import BaseActivity, create_activity, Session from .activity import BaseActivity, create_activity, Session
from .user import User, ClientUser from .user import User, ClientUser, Note
from .emoji import Emoji from .emoji import Emoji
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
@ -1263,6 +1263,20 @@ class ConnectionState:
if self.user: if self.user:
self.user._full_update(data) self.user._full_update(data)
def parse_user_note_update(self, data: gw.UserNoteUpdateEvent) -> None:
# The gateway does not provide note objects on READY anymore,
# so we cannot have (old, new) event dispatches
user_id = int(data['id'])
text = data['note']
user = self.get_user(user_id)
if user:
note = user.note
note._value = text
else:
note = Note(self, user_id, note=text)
self.dispatch('note_update', note)
# def parse_user_settings_update(self, data) -> None: # def parse_user_settings_update(self, data) -> None:
# new_settings = self.settings # new_settings = self.settings
# old_settings = copy.copy(new_settings) # old_settings = copy.copy(new_settings)

31
discord/types/application.py

@ -44,8 +44,11 @@ class BaseApplication(TypedDict):
summary: NotRequired[Literal['']] summary: NotRequired[Literal['']]
class IntegrationApplication(BaseApplication): class MetadataApplication(BaseApplication):
bot: NotRequired[PartialUser] bot: NotRequired[PartialUser]
class IntegrationApplication(MetadataApplication):
role_connections_verification_url: NotRequired[Optional[str]] role_connections_verification_url: NotRequired[Optional[str]]
@ -76,6 +79,7 @@ class PartialApplication(BaseApplication):
eula_id: NotRequired[Snowflake] eula_id: NotRequired[Snowflake]
embedded_activity_config: NotRequired[EmbeddedActivityConfig] embedded_activity_config: NotRequired[EmbeddedActivityConfig]
guild: NotRequired[PartialGuild] guild: NotRequired[PartialGuild]
install_params: NotRequired[ApplicationInstallParams]
class ApplicationDiscoverability(TypedDict): class ApplicationDiscoverability(TypedDict):
@ -234,6 +238,11 @@ class EmbeddedActivityConfig(TypedDict):
supported_platforms: List[EmbeddedActivityPlatform] supported_platforms: List[EmbeddedActivityPlatform]
class ApplicationInstallParams(TypedDict):
scopes: List[str]
permissions: int
class ActiveDeveloperWebhook(TypedDict): class ActiveDeveloperWebhook(TypedDict):
channel_id: Snowflake channel_id: Snowflake
webhook_id: Snowflake webhook_id: Snowflake
@ -241,3 +250,23 @@ class ActiveDeveloperWebhook(TypedDict):
class ActiveDeveloperResponse(TypedDict): class ActiveDeveloperResponse(TypedDict):
follower: ActiveDeveloperWebhook follower: ActiveDeveloperWebhook
class RoleConnectionMetadata(TypedDict):
type: Literal[1, 2, 3, 4, 5, 6, 7, 8]
key: str
name: str
description: str
name_localizations: NotRequired[Dict[str, str]]
description_localizations: NotRequired[Dict[str, str]]
class PartialRoleConnection(TypedDict):
platform_name: Optional[str]
platform_username: Optional[str]
metadata: Dict[str, str]
class RoleConnection(PartialRoleConnection):
application: MetadataApplication
application_metadata: List[RoleConnectionMetadata]

5
discord/types/gateway.py

@ -503,3 +503,8 @@ class PassiveUpdateEvent(TypedDict):
class GuildApplicationCommandIndexUpdateEvent(TypedDict): class GuildApplicationCommandIndexUpdateEvent(TypedDict):
guild_id: Snowflake guild_id: Snowflake
application_command_counts: ApplicationCommandCounts application_command_counts: ApplicationCommandCounts
class UserNoteUpdateEvent(TypedDict):
id: Snowflake
note: str

19
discord/types/member.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from typing import Optional, TypedDict from typing import Optional, TypedDict
from .snowflake import SnowflakeList from .snowflake import SnowflakeList
from .user import User from .user import PartialUser
class Nickname(TypedDict): class Nickname(TypedDict):
@ -40,27 +40,30 @@ class PartialMember(TypedDict):
class Member(PartialMember, total=False): class Member(PartialMember, total=False):
avatar: str avatar: Optional[str]
user: User user: PartialUser
nick: str nick: str
premium_since: Optional[str] premium_since: Optional[str]
pending: bool pending: bool
permissions: str
communication_disabled_until: str communication_disabled_until: str
class _OptionalMemberWithUser(PartialMember, total=False): class _OptionalMemberWithUser(PartialMember, total=False):
avatar: str avatar: Optional[str]
nick: str nick: str
premium_since: Optional[str] premium_since: Optional[str]
pending: bool pending: bool
permissions: str
communication_disabled_until: str communication_disabled_until: str
class MemberWithUser(_OptionalMemberWithUser): class MemberWithUser(_OptionalMemberWithUser):
user: User user: PartialUser
class UserWithMember(User, total=False): class PrivateMember(MemberWithUser):
bio: str
banner: Optional[str]
class UserWithMember(PartialUser, total=False):
member: _OptionalMemberWithUser member: _OptionalMemberWithUser

73
discord/types/profile.py

@ -0,0 +1,73 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import List, Optional, TypedDict
from typing_extensions import NotRequired
from .application import ApplicationInstallParams, RoleConnection
from .member import PrivateMember as ProfileMember
from .snowflake import Snowflake
from .user import APIUser, PartialConnection, PremiumType
class ProfileUser(APIUser):
bio: str
class ProfileMetadata(TypedDict):
guild_id: NotRequired[int]
bio: NotRequired[str]
banner: NotRequired[Optional[str]]
accent_color: NotRequired[Optional[int]]
theme_colors: NotRequired[List[int]]
class MutualGuild(TypedDict):
id: Snowflake
nick: Optional[str]
class ProfileApplication(TypedDict):
id: Snowflake
verified: bool
popular_application_command_ids: NotRequired[List[Snowflake]]
primary_sku_id: NotRequired[Snowflake]
flags: int
custom_install_url: NotRequired[str]
install_params: NotRequired[ApplicationInstallParams]
class Profile(TypedDict):
user: ProfileUser
user_profile: Optional[ProfileMetadata]
guild_member: NotRequired[ProfileMember]
guild_member_profile: NotRequired[Optional[ProfileMetadata]]
mutual_guilds: NotRequired[List[MutualGuild]]
mutual_friends_count: NotRequired[int]
connected_accounts: List[PartialConnection]
application_role_connections: NotRequired[List[RoleConnection]]
premium_type: Optional[PremiumType]
premium_since: Optional[str]
premium_guild_since: Optional[str]
application: NotRequired[ProfileApplication]

15
discord/types/user.py

@ -67,7 +67,12 @@ ConnectionVisibilty = Literal[0, 1]
PremiumType = Literal[0, 1, 2, 3] PremiumType = Literal[0, 1, 2, 3]
class User(PartialUser, total=False): class APIUser(PartialUser):
banner: Optional[str]
accent_color: Optional[int]
class User(APIUser, total=False):
mfa_enabled: bool mfa_enabled: bool
locale: str locale: str
verified: bool verified: bool
@ -76,8 +81,6 @@ class User(PartialUser, total=False):
purchased_flags: int purchased_flags: int
premium_usage_flags: int premium_usage_flags: int
premium_type: PremiumType premium_type: PremiumType
banner: Optional[str]
accent_color: Optional[int]
bio: str bio: str
analytics_token: str analytics_token: str
phone: Optional[str] phone: Optional[str]
@ -147,3 +150,9 @@ class GuildAffinity(TypedDict):
class GuildAffinities(TypedDict): class GuildAffinities(TypedDict):
guild_affinities: List[GuildAffinity] guild_affinities: List[GuildAffinity]
class Note(TypedDict):
note: str
user_id: Snowflake
note_user_id: Snowflake

114
discord/user.py

@ -40,7 +40,7 @@ from .enums import (
from .errors import ClientException, NotFound from .errors import ClientException, NotFound
from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags
from .relationship import Relationship from .relationship import Relationship
from .utils import _bytes_to_base64_data, _get_as_snowflake, copy_doc, snowflake_time, MISSING from .utils import _bytes_to_base64_data, _get_as_snowflake, cached_slot_property, copy_doc, snowflake_time, MISSING
from .voice_client import VoiceClient from .voice_client import VoiceClient
if TYPE_CHECKING: if TYPE_CHECKING:
@ -74,8 +74,6 @@ __all__ = (
class Note: class Note:
"""Represents a Discord note. """Represents a Discord note.
.. versionadded:: 2.0
.. container:: operations .. container:: operations
.. describe:: x == y .. describe:: x == y
@ -97,19 +95,21 @@ class Note:
.. describe:: len(x) .. describe:: len(x)
Returns the note's length. Returns the note's length.
.. versionadded:: 1.9
Attributes Attributes
----------- -----------
user_id: :class:`int` user_id: :class:`int`
The user ID the note is for. The user ID the note is for.
""" """
__slots__ = ('_state', '_note', 'user_id', '_user') __slots__ = ('_state', '_value', 'user_id', '_user')
def __init__( def __init__(
self, state: ConnectionState, user_id: int, *, user: Optional[User] = None, note: Optional[str] = MISSING self, state: ConnectionState, user_id: int, *, user: Optional[User] = None, note: Optional[str] = MISSING
) -> None: ) -> None:
self._state = state self._state = state
self._note: Optional[str] = note self._value: Optional[str] = note
self.user_id: int = user_id self.user_id: int = user_id
self._user: Optional[User] = user self._user: Optional[User] = user
@ -124,9 +124,9 @@ class Note:
ClientException ClientException
Attempted to access note without fetching it. Attempted to access note without fetching it.
""" """
if self._note is MISSING: if self._value is MISSING:
raise ClientException('Note is not fetched') raise ClientException('Note is not fetched')
return self._note return self._value
@property @property
def value(self) -> Optional[str]: def value(self) -> Optional[str]:
@ -143,8 +143,8 @@ class Note:
@property @property
def user(self) -> Optional[User]: def user(self) -> Optional[User]:
"""Optional[:class:`User`]: Returns the :class:`User` the note belongs to.""" """Optional[:class:`User`]: Returns the user the note belongs to."""
return self._user or self._state.get_user(self.user_id) return self._state.get_user(self.user_id) or self._user
async def fetch(self) -> Optional[str]: async def fetch(self) -> Optional[str]:
"""|coro| """|coro|
@ -163,16 +163,18 @@ class Note:
""" """
try: 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'] self._value = data['note']
return data['note'] return data['note']
except NotFound: # 404 = no note except NotFound:
self._note = None # A 404 means the note doesn't exist
# However, this is bad UX, so we just return None
self._value = None
return None return None
async def edit(self, note: Optional[str]) -> None: async def edit(self, note: Optional[str]) -> None:
"""|coro| """|coro|
Changes the note. Modifies the note. Can be at most 256 characters.
Raises Raises
------- -------
@ -180,7 +182,7 @@ class Note:
Changing the note failed. 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 self._value = note or ''
async def delete(self) -> None: async def delete(self) -> None:
"""|coro| """|coro|
@ -196,14 +198,14 @@ class Note:
def __repr__(self) -> str: def __repr__(self) -> str:
base = f'<Note user={self.user!r}' base = f'<Note user={self.user!r}'
note = self._note note = self._value
if note is not MISSING: if note is not MISSING:
note = note or '' note = note or ''
base += f' note={note!r}' return f'{base} note={note!r}>'
return base + '>' return f'{base}>'
def __str__(self) -> str: def __str__(self) -> str:
note = self._note note = self._value
if note is MISSING: if note is MISSING:
raise ClientException('Note is not fetched') raise ClientException('Note is not fetched')
return note or '' return note or ''
@ -212,15 +214,15 @@ class Note:
return bool(str(self)) return bool(str(self))
def __eq__(self, other: object) -> bool: 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.user_id == other.user_id
def __ne__(self, other: object) -> bool: def __ne__(self, other: object) -> bool:
if isinstance(other, Note): if isinstance(other, Note):
return self._note != other._note or self.user_id != other.user_id return self._value != other._value or self.user_id != other.user_id
return True return True
def __hash__(self) -> int: def __hash__(self) -> int:
return hash((self._note, self.user_id)) return hash((self._value, self.user_id))
def __len__(self) -> int: def __len__(self) -> int:
note = str(self) note = str(self)
@ -244,6 +246,7 @@ class BaseUser(_UserTag):
'bot', 'bot',
'system', 'system',
'_public_flags', '_public_flags',
'_cs_note',
'_state', '_state',
) )
@ -313,7 +316,7 @@ class BaseUser(_UserTag):
return self return self
def _to_minimal_user_json(self) -> PartialUserPayload: def _to_minimal_user_json(self) -> PartialUserPayload:
user: UserPayload = { user: PartialUserPayload = {
'username': self.name, 'username': self.name,
'id': self.id, 'id': self.id,
'avatar': self._avatar, 'avatar': self._avatar,
@ -473,6 +476,18 @@ class BaseUser(_UserTag):
""" """
return self.name return self.name
@cached_slot_property('_cs_note')
def note(self) -> Note:
""":class:`Note`: Returns an object representing the user's note.
.. versionadded:: 2.0
.. note::
The underlying note is cached and updated from gateway events.
"""
return Note(self._state, self.id, user=self) # type: ignore
def mentioned_in(self, message: Message) -> bool: def mentioned_in(self, message: Message) -> bool:
"""Checks if the user is mentioned in the specified message. """Checks if the user is mentioned in the specified message.
@ -1012,18 +1027,35 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
""" """
await self._state.http.send_friend_request(self.name, self.discriminator) await self._state.http.send_friend_request(self.name, self.discriminator)
async def profile(self, *, with_mutuals: bool = True, fetch_note: bool = True) -> UserProfile: async def profile(
self,
*,
with_mutual_guilds: bool = True,
with_mutual_friends_count: bool = False,
with_mutual_friends: bool = True,
) -> UserProfile:
"""|coro| """|coro|
Gets the user's profile. A shorthand method to retrieve a :class:`UserProfile` for the user.
Parameters Parameters
------------ ------------
with_mutuals: :class:`bool` with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds and friends. Whether to fetch mutual guilds.
This fills in :attr:`.UserProfile.mutual_guilds` & :attr:`.UserProfile.mutual_friends`. This fills in :attr:`UserProfile.mutual_guilds`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note. .. versionadded:: 2.0
with_mutual_friends_count: :class:`bool`
Whether to fetch the number of mutual friends.
This fills in :attr:`UserProfile.mutual_friends_count`.
.. versionadded:: 2.0
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`UserProfile.mutual_friends` and :attr:`UserProfile.mutual_friends_count`,
but requires an extra API call.
.. versionadded:: 2.0
Raises Raises
------- -------
@ -1037,21 +1069,9 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
:class:`UserProfile` :class:`UserProfile`
The profile of the user. The profile of the user.
""" """
from .profile import UserProfile return await self._state.client.fetch_user_profile(
self.id,
user_id = self.id with_mutual_guilds=with_mutual_guilds,
state = self._state with_mutual_friends_count=with_mutual_friends_count,
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals) with_mutual_friends=with_mutual_friends,
)
if with_mutuals:
if not data['user'].get('bot', False):
data['mutual_friends'] = await self._state.http.get_mutual_friends(user_id)
else:
data['mutual_friends'] = []
profile = UserProfile(state=state, data=data)
if fetch_note:
await profile.note.fetch()
return profile

15
docs/api.rst

@ -665,6 +665,16 @@ Relationships
:param after: The updated relationship. :param after: The updated relationship.
:type after: :class:`Relationship` :type after: :class:`Relationship`
Notes
~~~~~~
.. function:: on_note_update(note)
Called when a :class:`User`\'s note is updated.
:param note: The note that was updated.
:type note: :class:`Note`
Calls Calls
~~~~~ ~~~~~
@ -6787,6 +6797,11 @@ Guild
.. autoclass:: UserGuild() .. autoclass:: UserGuild()
:members: :members:
.. attributetable:: MutualGuild
.. autoclass:: MutualGuild()
:members:
.. class:: BanEntry .. class:: BanEntry
A namedtuple which represents a ban returned from :meth:`~Guild.bans`. A namedtuple which represents a ban returned from :meth:`~Guild.bans`.

Loading…
Cancel
Save