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

53
discord/client.py

@ -2162,13 +2162,20 @@ class Client:
return User(state=self._connection, data=data)
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:
"""|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).
.. versionchanged:: 2.0
@ -2179,11 +2186,22 @@ class Client:
------------
user_id: :class:`int`
The ID of the user to fetch their profile for.
with_mutuals: :class:`bool`
Whether to fetch mutual guilds and friends.
This fills in :attr:`.UserProfile.mutual_guilds` & :attr:`.UserProfile.mutual_friends`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note.
with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds.
This fills in :attr:`.UserProfile.mutual_guilds`.
.. 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
-------
@ -2200,19 +2218,14 @@ class Client:
The profile of the user.
"""
state = self._connection
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals)
if with_mutuals:
if not data['user'].get('bot', False):
data['mutual_friends'] = await state.http.get_mutual_friends(user_id)
else:
data['mutual_friends'] = []
profile = UserProfile(state=state, data=data)
if fetch_note:
await profile.note.fetch()
data = await state.http.get_user_profile(
user_id, with_mutual_guilds=with_mutual_guilds, with_mutual_friends_count=with_mutual_friends_count
)
mutual_friends = None
if with_mutual_friends and not data['user'].get('bot', False):
mutual_friends = await state.http.get_mutual_friends(user_id)
return profile
return UserProfile(state=state, data=data, mutual_friends=mutual_friends)
async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]:
"""|coro|

56
discord/guild.py

@ -2333,32 +2333,45 @@ class Guild(Hashable):
return Member(data=data, state=self._state, guild=self)
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:
"""|coro|
Gets an arbitrary member's profile.
Retrieves a :class:`.MemberProfile` from a guild ID, and a member ID.
.. versionadded:: 2.0
Parameters
------------
member_id: :class:`int`
The ID of the member to fetch their profile for.
with_mutuals: :class:`bool`
Whether to fetch mutual guilds and friends.
This fills in :attr:`.MemberProfile.mutual_guilds` & :attr:`.MemberProfile.mutual_friends`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note.
with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds.
This fills in :attr:`.MemberProfile.mutual_guilds`.
with_mutual_friends_count: :class:`bool`
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
-------
NotFound
A user with this ID does not exist.
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
Fetching the profile failed.
InvalidData
The member is not in this guild.
The member is not in this guild or has blocked you.
Returns
--------
@ -2366,23 +2379,18 @@ class Guild(Hashable):
The profile of the member.
"""
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:
raise InvalidData('Member not in this guild')
if with_mutuals:
if not data['user'].get('bot', False):
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()
raise InvalidData('Member has blocked you')
mutual_friends = None
if with_mutual_friends and not data['user'].get('bot', False):
mutual_friends = await state.http.get_mutual_friends(member_id)
return profile
return MemberProfile(state=state, data=data, mutual_friends=mutual_friends, guild=self)
async def fetch_ban(self, user: Snowflake) -> BanEntry:
"""|coro|

23
discord/http.py

@ -97,6 +97,7 @@ if TYPE_CHECKING:
member,
message,
payments,
profile,
promotions,
template,
role,
@ -4069,14 +4070,22 @@ class HTTPClient:
value = '{0}?encoding={1}&v={2}'
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))
def get_user_profile(
self, user_id: Snowflake, guild_id: Snowflake = MISSING, *, with_mutual_guilds: bool = True
): # TODO: return type
params: Dict[str, Any] = {'with_mutual_guilds': str(with_mutual_guilds).lower()}
if guild_id is not MISSING:
self,
user_id: Snowflake,
guild_id: Optional[Snowflake] = None,
*,
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
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
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'))
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))
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 .flags import PublicUserFlags
from .guild import Guild
from .profile import MemberProfile
from .types.activity import (
PartialPresenceUpdate,
)
@ -1107,3 +1108,54 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
Sending the friend request failed.
"""
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 .member import Member
from .mixins import Hashable
from .user import Note, User
from .user import User
if TYPE_CHECKING:
from datetime import datetime
from .guild import Guild
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__ = (
'ApplicationProfile',
'MutualGuild',
'UserProfile',
'MemberProfile',
)
@ -52,23 +59,29 @@ __all__ = (
class Profile:
if TYPE_CHECKING:
id: int
application_id: Optional[int]
bot: bool
_state: ConnectionState
def __init__(self, **kwargs) -> None: # TODO: type data
data = kwargs.pop('data')
def __init__(self, **kwargs) -> None:
data: ProfilePayload = kwargs.pop('data')
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
kwargs['data'] = member
else:
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)
state = self._state
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
self.bio: Optional[str] = user['bio'] or None
# 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)
@ -76,36 +89,42 @@ class Profile:
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
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.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.mutual_guilds: Optional[List[Guild]] = self._parse_mutual_guilds(data.get('mutual_guilds'))
self.mutual_friends: Optional[List[User]] = self._parse_mutual_friends(data.get('mutual_friends'))
self.mutual_guilds: Optional[List[MutualGuild]] = (
[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
def _parse_mutual_guilds(self, mutual_guilds) -> Optional[List[Guild]]:
if mutual_guilds is None:
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]]:
def _parse_mutual_friends(self, mutual_friends: List[PartialUserPayload]) -> Optional[List[User]]:
if self.bot:
# Bots don't have friends
return []
if mutual_friends is None:
return
state = self._state
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
def premium(self) -> bool:
""":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.
"""
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.verified: bool = data.get('verified', False)
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'
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):
"""Represents a Discord user's profile.
@ -209,7 +281,7 @@ class UserProfile(Profile, User):
Attributes
-----------
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`]
The user's "about me" field. Could be ``None``.
premium_type: Optional[:class:`PremiumType`]
@ -217,23 +289,41 @@ class UserProfile(Profile, User):
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.
boosting_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first boosted a guild.
premium_guild_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first Nitro boosted a guild.
connections: Optional[List[:class:`PartialConnection`]]
The connected accounts that show up on the profile.
note: :class:`Note`
Represents the note on the profile.
mutual_guilds: Optional[List[:class:`Guild`]]
mutual_guilds: Optional[List[:class:`MutualGuild`]]
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`]]
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:
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):
"""Represents a Discord member's profile.
@ -265,7 +355,7 @@ class MemberProfile(Profile, Member):
Attributes
-----------
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`]
The user's "about me" field. Could be ``None``.
guild_bio: Optional[:class:`str`]
@ -275,6 +365,7 @@ class MemberProfile(Profile, Member):
"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.
@ -283,14 +374,13 @@ class MemberProfile(Profile, Member):
``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 any guild.
premium_guild_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first Nitro boosted a guild.
connections: Optional[List[:class:`PartialConnection`]]
The connected accounts that show up on the profile.
note: :class:`Note`
Represents the note on the profile.
mutual_guilds: Optional[List[:class:`Guild`]]
mutual_guilds: Optional[List[:class:`MutualGuild`]]
A list of guilds that you share with the user.
``None`` if you didn't fetch mutuals.
mutual_friends: Optional[List[:class:`User`]]
@ -298,8 +388,24 @@ 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)
__slots__ = (
'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']
self._banner: Optional[str] = member.get('banner')
self.guild_bio: Optional[str] = member.get('bio') or None
@ -328,3 +434,13 @@ class MemberProfile(Profile, Member):
if self._banner is None:
return None
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 .guild import ApplicationCommandCounts, Guild
from .activity import BaseActivity, create_activity, Session
from .user import User, ClientUser
from .user import User, ClientUser, Note
from .emoji import Emoji
from .mentions import AllowedMentions
from .partial_emoji import PartialEmoji
@ -1263,6 +1263,20 @@ class ConnectionState:
if self.user:
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:
# new_settings = self.settings
# old_settings = copy.copy(new_settings)

31
discord/types/application.py

@ -44,8 +44,11 @@ class BaseApplication(TypedDict):
summary: NotRequired[Literal['']]
class IntegrationApplication(BaseApplication):
class MetadataApplication(BaseApplication):
bot: NotRequired[PartialUser]
class IntegrationApplication(MetadataApplication):
role_connections_verification_url: NotRequired[Optional[str]]
@ -76,6 +79,7 @@ class PartialApplication(BaseApplication):
eula_id: NotRequired[Snowflake]
embedded_activity_config: NotRequired[EmbeddedActivityConfig]
guild: NotRequired[PartialGuild]
install_params: NotRequired[ApplicationInstallParams]
class ApplicationDiscoverability(TypedDict):
@ -234,6 +238,11 @@ class EmbeddedActivityConfig(TypedDict):
supported_platforms: List[EmbeddedActivityPlatform]
class ApplicationInstallParams(TypedDict):
scopes: List[str]
permissions: int
class ActiveDeveloperWebhook(TypedDict):
channel_id: Snowflake
webhook_id: Snowflake
@ -241,3 +250,23 @@ class ActiveDeveloperWebhook(TypedDict):
class ActiveDeveloperResponse(TypedDict):
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):
guild_id: Snowflake
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 .snowflake import SnowflakeList
from .user import User
from .user import PartialUser
class Nickname(TypedDict):
@ -40,27 +40,30 @@ class PartialMember(TypedDict):
class Member(PartialMember, total=False):
avatar: str
user: User
avatar: Optional[str]
user: PartialUser
nick: str
premium_since: Optional[str]
pending: bool
permissions: str
communication_disabled_until: str
class _OptionalMemberWithUser(PartialMember, total=False):
avatar: str
avatar: Optional[str]
nick: str
premium_since: Optional[str]
pending: bool
permissions: str
communication_disabled_until: str
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

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]
class User(PartialUser, total=False):
class APIUser(PartialUser):
banner: Optional[str]
accent_color: Optional[int]
class User(APIUser, total=False):
mfa_enabled: bool
locale: str
verified: bool
@ -76,8 +81,6 @@ class User(PartialUser, total=False):
purchased_flags: int
premium_usage_flags: int
premium_type: PremiumType
banner: Optional[str]
accent_color: Optional[int]
bio: str
analytics_token: str
phone: Optional[str]
@ -147,3 +150,9 @@ class GuildAffinity(TypedDict):
class GuildAffinities(TypedDict):
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 .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags
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
if TYPE_CHECKING:
@ -74,8 +74,6 @@ __all__ = (
class Note:
"""Represents a Discord note.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
@ -97,19 +95,21 @@ class Note:
.. describe:: len(x)
Returns the note's length.
.. versionadded:: 1.9
Attributes
-----------
user_id: :class:`int`
The user ID the note is for.
"""
__slots__ = ('_state', '_note', 'user_id', '_user')
__slots__ = ('_state', '_value', 'user_id', '_user')
def __init__(
self, state: ConnectionState, user_id: int, *, user: Optional[User] = None, note: Optional[str] = MISSING
) -> None:
self._state = state
self._note: Optional[str] = note
self._value: Optional[str] = note
self.user_id: int = user_id
self._user: Optional[User] = user
@ -124,9 +124,9 @@ class Note:
ClientException
Attempted to access note without fetching it.
"""
if self._note is MISSING:
if self._value is MISSING:
raise ClientException('Note is not fetched')
return self._note
return self._value
@property
def value(self) -> Optional[str]:
@ -143,8 +143,8 @@ class Note:
@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)
"""Optional[:class:`User`]: Returns the user the note belongs to."""
return self._state.get_user(self.user_id) or self._user
async def fetch(self) -> Optional[str]:
"""|coro|
@ -163,16 +163,18 @@ class Note:
"""
try:
data = await self._state.http.get_note(self.user_id)
self._note = data['note']
self._value = data['note']
return data['note']
except NotFound: # 404 = no note
self._note = None
except NotFound:
# A 404 means the note doesn't exist
# However, this is bad UX, so we just return None
self._value = None
return None
async def edit(self, note: Optional[str]) -> None:
"""|coro|
Changes the note.
Modifies the note. Can be at most 256 characters.
Raises
-------
@ -180,7 +182,7 @@ class Note:
Changing the note failed.
"""
await self._state.http.set_note(self.user_id, note=note)
self._note = note
self._value = note or ''
async def delete(self) -> None:
"""|coro|
@ -196,14 +198,14 @@ class Note:
def __repr__(self) -> str:
base = f'<Note user={self.user!r}'
note = self._note
note = self._value
if note is not MISSING:
note = note or ''
base += f' note={note!r}'
return base + '>'
return f'{base} note={note!r}>'
return f'{base}>'
def __str__(self) -> str:
note = self._note
note = self._value
if note is MISSING:
raise ClientException('Note is not fetched')
return note or ''
@ -212,15 +214,15 @@ class Note:
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.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._value != other._value or self.user_id != other.user_id
return True
def __hash__(self) -> int:
return hash((self._note, self.user_id))
return hash((self._value, self.user_id))
def __len__(self) -> int:
note = str(self)
@ -244,6 +246,7 @@ class BaseUser(_UserTag):
'bot',
'system',
'_public_flags',
'_cs_note',
'_state',
)
@ -313,7 +316,7 @@ class BaseUser(_UserTag):
return self
def _to_minimal_user_json(self) -> PartialUserPayload:
user: UserPayload = {
user: PartialUserPayload = {
'username': self.name,
'id': self.id,
'avatar': self._avatar,
@ -473,6 +476,18 @@ class BaseUser(_UserTag):
"""
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:
"""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)
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|
Gets the user's profile.
A shorthand method to retrieve a :class:`UserProfile` for the user.
Parameters
------------
with_mutuals: :class:`bool`
Whether to fetch mutual guilds and friends.
This fills in :attr:`.UserProfile.mutual_guilds` & :attr:`.UserProfile.mutual_friends`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note.
with_mutual_guilds: :class:`bool`
Whether to fetch mutual guilds.
This fills in :attr:`UserProfile.mutual_guilds`.
.. 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
-------
@ -1037,21 +1069,9 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
:class:`UserProfile`
The profile of the user.
"""
from .profile import UserProfile
user_id = self.id
state = self._state
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals)
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
return await self._state.client.fetch_user_profile(
self.id,
with_mutual_guilds=with_mutual_guilds,
with_mutual_friends_count=with_mutual_friends_count,
with_mutual_friends=with_mutual_friends,
)

15
docs/api.rst

@ -665,6 +665,16 @@ Relationships
:param after: The updated 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
~~~~~
@ -6787,6 +6797,11 @@ Guild
.. autoclass:: UserGuild()
:members:
.. attributetable:: MutualGuild
.. autoclass:: MutualGuild()
:members:
.. class:: BanEntry
A namedtuple which represents a ban returned from :meth:`~Guild.bans`.

Loading…
Cancel
Save