Browse Source

Migrate user.py, refactor a few things

pull/10109/head
dolfies 4 years ago
parent
commit
3e4ad5d651
  1. 83
      discord/client.py
  2. 732
      discord/user.py

83
discord/client.py

@ -39,7 +39,7 @@ from .template import Template
from .widget import Widget
from .guild import Guild
from .emoji import Emoji
from .channel import _threaded_channel_factory, PartialMessageable
from .channel import _private_channel_factory, _threaded_channel_factory, GroupChannel, PartialMessageable
from .enums import ChannelType, Status, VoiceRegion, try_enum
from .mentions import AllowedMentions
from .errors import *
@ -55,7 +55,6 @@ from .backoff import ExponentialBackoff
from .webhook import Webhook
from .iterators import GuildIterator
from .appinfo import AppInfo
from .ui.view import View
from .stage_instance import StageInstance
from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@ -323,7 +322,6 @@ class Client:
def _schedule_event(self, coro: Callable[..., Coroutine[Any, Any, Any]], event_name: str, *args: Any, **kwargs: Any) -> asyncio.Task:
wrapped = self._run_event(coro, event_name, *args, **kwargs)
# Schedules the task
return asyncio.create_task(wrapped, name=f'discord.py: {event_name}')
def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None:
@ -402,7 +400,6 @@ class Client:
initial: :class:`bool`
Whether this IDENTIFY is the first initial IDENTIFY.
"""
pass
# Login state management
@ -1607,15 +1604,28 @@ class Client:
"""
data = await self.http.get_sticker(sticker_id)
cls, _ = _sticker_factory(data['type']) # type: ignore
return cls(state=self._connection, data=data) # type: ignore
return cls(state=self._connection, data=data) # type: ignore
async def fetch_premium_sticker_packs(self) -> List[StickerPack]:
async def fetch_sticker_packs(
self, *, country='US', locale='en-US', *, payment_source_id: int = MISSING
) -> List[StickerPack]:
"""|coro|
Retrieves all available premium sticker packs.
Retrieves all available default sticker packs.
.. versionadded:: 2.0
Parameters
-----------
country: :class:`str`
ISO 3166 country code to fetch the sticker packs for.
Defaults to ``US``.
locale: :class:`str`
ISO 639 language code the name and description should be in.
Defaults to ``en-US``.
payment_source_id: :class:`int`
Unknown.
Raises
-------
:exc:`.HTTPException`
@ -1626,7 +1636,7 @@ class Client:
List[:class:`.StickerPack`]
All available premium sticker packs.
"""
data = await self.http.list_premium_sticker_packs()
data = await self.http.list_premium_sticker_packs(country, locale, payment_source_id)
return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']]
async def fetch_notes(self) -> List[Note]:
@ -1645,7 +1655,7 @@ class Client:
All your notes.
"""
state = self._connection
data = await self.http.get_notes()
data = await state.http.get_notes()
return [Note(state, int(id), note=note) for id, note in data.items()]
async def fetch_note(self, user_id: int) -> Note:
@ -1668,11 +1678,28 @@ class Client:
:class:`Note`
The note you requested.
"""
try:
data = await self.http.get_note(user_id)
except NotFound:
data = {'note': 0}
return Note(self._connection, int(user_id), note=data['note'])
note = Note(self._connection, int(user_id))
await note.fetch()
return note
async def fetch_private_channels(self) -> List[PrivateChannel]:
"""|coro
Retrieves all your private channels.
Raises
-------
:exc:`.HTTPException`
Retreiving your private channels failed.
Returns
--------
List[:class:`PrivateChannel`]
All your private channels.
"""
channels = await self._state.http.get_private_channels()
state = self._connection
return [_private_channel_factory(data['type'])(me=self.user, data=data, state=state) for data in channels]
async def create_dm(self, user: Snowflake) -> DMChannel:
"""|coro|
@ -1701,3 +1728,31 @@ class Client:
data = await state.http.start_private_message(user.id)
return state.add_dm_channel(data)
async def create_group(self, *recipients) -> GroupChannel:
r"""|coro|
Creates a group direct message with the recipients
provided. These recipients must be have a relationship
of type :attr:`RelationshipType.friend`.
Parameters
-----------
\*recipients: :class:`User`
An argument :class:`list` of :class:`User` to have in
your group.
Raises
-------
HTTPException
Failed to create the group direct message.
Returns
-------
:class:`GroupChannel`
The new group channel.
"""
users = [str(u.id) for u in recipients]
state = self._connection
data = await state.http.start_group(users)
return GroupChannel(me=self.user, data=data, state=state)

732
discord/user.py

@ -24,20 +24,28 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Any, Dict, List, Optional, Type, TypeVar, TYPE_CHECKING
from copy import copy
from typing import Any, Dict, List, Optional, Type, TypeVar, TYPE_CHECKING, Union
import discord.abc
from types.snowflake import Snowflake
from .asset import Asset
from .colour import Colour
from .enums import DefaultAvatar
from .enums import DefaultAvatar, HypeSquadHouse, PremiumType, RelationshipAction, RelationshipType, try_enum, UserFlags
from .errors import ClientException, NotFound
from .flags import PublicUserFlags
from .utils import snowflake_time, _bytes_to_base64_data, MISSING
from .object import Object
from .relationship import Relationship
from .settings import Settings
from .utils import _bytes_to_base64_data, cached_slot_property, parse_time, snowflake_time, MISSING
if TYPE_CHECKING:
from datetime import datetime
from .call import Call
from .channel import DMChannel
from .guild import Guild
from .member import VoiceState
from .message import Message
from .state import ConnectionState
from .types.channel import DMChannel as DMChannelPayload
@ -52,6 +60,251 @@ __all__ = (
BU = TypeVar('BU', bound='BaseUser')
class Note:
"""Represents a Discord note."""
__slots__ = ('_state', '_note', '_user_id', '_user')
def __init__(
self, state: ConnectionState, user_id: int, *, user: BaseUser = MISSING, note: Optional[str] = MISSING
) -> None:
self._state = state
self._user_id = user_id
self._note = note
if user is not MISSING:
self._user: Union[User, Object] = user
@property
def note(self) -> Optional[str]:
"""Returns the note.
Raises
-------
ClientException
Attempted to access note without fetching it.
"""
if self._note is MISSING:
raise ClientException('Note is not fetched')
return self._note
@cached_slot_property('_user')
def user(self) -> Union[User, Object]:
"""Returns the :class:`User` the note belongs to.
If the user isn't in the cache, it returns a
:class:`Object` instead.
"""
user_id = self._user_id
user = self._state.get_user(user_id)
if user is None:
user = Object(user_id)
return user
async def fetch(self) -> Optional[str]:
"""|coro|
Retrieves the note.
Raises
-------
HTTPException
Fetching the note failed.
Returns
--------
Optional[:class:`str`]
The note or ``None`` if it doesn't exist.
"""
try:
data = await self._state.http.get_note(self.user.id)
self._note = data['note']
return data['note']
except NotFound: # 404 = no note :(
self._note = None
return None
async def edit(self, note: Optional[str]) -> None:
"""|coro|
Changes the note.
Raises
-------
HTTPException
Changing the note failed.
"""
await self._state.http.set_note(self._user_id, note=note)
self._note = note
async def delete(self) -> None:
"""|coro|
A shortcut to :meth:`.edit` that deletes the note.
Raises
-------
HTTPException
Deleting the note failed.
"""
await self.edit(None)
def __str__(self) -> str:
note = self._note
if note is MISSING:
raise ClientException('Note is not fetched')
elif note is None:
return ''
else:
return note
def __repr__(self) -> str:
base = f'<Note user={self.user!r}'
note = self._note
if note is not MISSING:
base += f' note={note or ""}>'
else:
base += '>'
return base
def __len__(self) -> int:
try:
return len(self._note)
except TypeError:
return 0
def __eq__(self, other) -> bool:
try:
return isinstance(other, Note) and self._note == other._note
except TypeError:
return False
def __ne__(self, other) -> bool:
return not self.__eq__(other)
def __bool__(self) -> bool:
try:
return bool(self._note)
except TypeError:
return False
class Profile:
"""Represents a Discord profile.
Attributes
----------
flags: :class:`int`
The user's flags. Will be its own class (like public_flags) in the future.
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
user: :class:`User`
The user the profile represents (with banner/accent_colour).
premium_since: Optional[:class:`datetime.datetime`]
A datetime object denoting how long a user has been premium (had Nitro).
Could be ``None``.
connected_accounts: Optional[List[:class:`dict`]]
The connected accounts that show up on the profile.
These are currently just the raw json, but this will change in the future.
note: :class:`Note`
Represents the note on the profile.
"""
__slots__ = (
'_state',
'user',
'flags',
'bio',
'premium_since',
'connected_accounts',
'note',
'mutual_guilds',
'mutual_friends',
)
def __init__(self, state: ConnectionState, data) -> None: # Type data
self._state = state
user = data['user']
self.flags: int = user.pop('flags', 0) # TODO: figure them all out and parse them
self.bio: Optional[str] = user.pop('bio') or None
self.user: User = User(data=user, state=state)
self.premium_since: datetime = parse_time(data['premium_since'])
self.connected_accounts: List[dict] = data['connected_accounts'] # TODO: parse these
self.note: Note = Note(state, self.user.id, user=self.user)
if 'mutual_guilds' in data:
self.mutual_guilds: List[Guild] = self._parse_mutual_guilds(data['mutual_guilds'])
if 'mutual_friends' in data: # TODO: maybe make Relationships
self.mutual_friends: List[User] = self._parse_mutual_friends(data['mutual_friends'])
def __str__(self) -> str:
return '{0.name}#{0.discriminator}'.format(self.user)
def __repr__(self) -> str:
return '<Profile user={0.user!r} bio={0.bio}>'.format(self)
def _parse_mutual_guilds(self, mutual_guilds) -> List[Guild]:
state = self._state
def get_guild(guild) -> Optional[Guild]:
return state._get_guild(int(guild['id']))
return list(filter(None, map(get_guild, mutual_guilds)))
def _parse_mutual_friends(self, mutual_friends) -> List[User]:
state = self._state
return [state.store_user(friend) for friend in mutual_friends]
@property
def nitro(self) -> bool:
return self.premium_since is not None
premium = nitro
def _has_flag(self, o) -> bool:
v = o.value
return (self.flags & v) == v
@property
def staff(self) -> bool:
return self._has_flag(UserFlags.staff)
@property
def partner(self) -> bool:
return self._has_flag(UserFlags.partner)
@property
def bug_hunter(self) -> bool:
return self._has_flag(UserFlags.bug_hunter)
@property
def early_supporter(self) -> bool:
return self._has_flag(UserFlags.early_supporter)
@property
def hypesquad(self) -> bool:
return self._has_flag(UserFlags.hypesquad)
@property
def hypesquad_house(self) -> HypeSquadHouse:
return self.hypesquad_houses[0]
@property
def hypesquad_houses(self) -> List[HypeSquadHouse]:
flags = (UserFlags.hypesquad_bravery, UserFlags.hypesquad_brilliance, UserFlags.hypesquad_balance)
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
@property
def team_user(self) -> bool:
return self._has_flag(UserFlags.team_user)
@property
def system(self) -> bool:
return self._has_flag(UserFlags.system)
class _UserTag:
__slots__ = ()
id: int
@ -139,8 +392,14 @@ class BaseUser(_UserTag):
'avatar': self._avatar,
'discriminator': self.discriminator,
'bot': self.bot,
'system': self.system,
}
@property
def voice(self) -> Optional[VoiceState]:
"""Optional[:class:`VoiceState`]: Returns the user's current voice state."""
return self._state._voice_state_for(self.id)
@property
def public_flags(self) -> PublicUserFlags:
""":class:`PublicUserFlags`: The publicly available flags the user has."""
@ -305,7 +564,11 @@ class ClientUser(BaseUser):
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
The user's discriminator.
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
avatar: Optional[:class:`str`]
The avatar hash the user has. Could be ``None``.
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@ -315,38 +578,112 @@ class ClientUser(BaseUser):
verified: :class:`bool`
Specifies if the user's email is verified.
email: Optional[:class:`str`]
The email of the user.
phone: Optional[:class:`int`]
The phone number of the user.
.. versionadded:: 1.9
locale: Optional[:class:`str`]
The IETF language tag used to identify the language the user is using.
mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working.
premium: :class:`bool`
Specifies if the user is a premium user (i.e. has Discord Nitro).
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.
note: :class:`Note`
The user's note. Not pre-fetched.
"""
__slots__ = ('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
if TYPE_CHECKING:
verified: bool
email: Optional[str]
phone: Optional[int]
locale: Optional[str]
mfa_enabled: bool
_flags: int
mfa_enabled: bool
premium: bool
premium_type: Optional[PremiumType]
bio: Optional[str]
def __init__(self, *, state: ConnectionState, data: UserPayload) -> None:
super().__init__(state=state, data=data)
self.note: Note = Note(state, self.id)
def __repr__(self) -> str:
return (
f'<ClientUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}'
f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled}>'
f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled} premium={self.premium}>'
)
def _update(self, data: UserPayload) -> None:
super()._update(data)
# There's actually an Optional[str] phone field as well but I won't use it
self.verified = data.get('verified', False)
self.email = data.get('email')
self.phone = data.get('phone')
self.locale = data.get('locale')
self._flags = data.get('flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False)
self.premium = data.get('premium', False)
self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
self.bio = data.get('bio') or None
def get_relationship(self, user_id: int) -> Relationship:
"""Retrieves the :class:`Relationship` if applicable.
async def edit(self, *, username: str = MISSING, avatar: bytes = MISSING) -> ClientUser:
Parameters
-----------
user_id: :class:`int`
The user ID to check if we have a relationship with them.
Returns
--------
Optional[:class:`Relationship`]
The relationship if available or ``None``.
"""
return self._state._relationships.get(user_id)
@property
def relationships(self) -> List[Relationship]:
"""List[:class:`User`]: Returns all the relationships that the user has."""
return list(self._state._relationships.values())
@property
def friends(self) -> List[Relationship]:
r"""List[:class:`User`]: Returns all the users that the user is friends with."""
return [r.user for r in self._state._relationships.values() if r.type is RelationshipType.friend]
@property
def blocked(self) -> List[Relationship]:
r"""List[:class:`User`]: Returns all the users that the user has blocked."""
return [r.user for r in self._state._relationships.values() if r.type is RelationshipType.blocked]
@property
def settings(self) -> Optional[Settings]:
"""Optional[:class:`Settings`]: Returns the user's settings."""
return self._state.settings
async def edit(
self,
*,
username: str = MISSING,
avatar: Optional[bytes] = MISSING,
password: str = MISSING,
new_password: str = MISSING,
email: str = MISSING,
house: Optional[HypeSquadHouse] = MISSING,
discriminator: Snowflake = MISSING,
banner: Optional[bytes] = MISSING,
accent_colour: Colour = MISSING,
accent_color: Colour = MISSING,
bio: Optional[str] = MISSING,
date_of_birth: datetime = MISSING,
) -> ClientUser:
"""|coro|
Edits the current profile of the client.
@ -358,43 +695,265 @@ class ClientUser(BaseUser):
then the file must be opened via ``open('some_filename', 'rb')`` and
the :term:`py:bytes-like object` is given through the use of ``fp.read()``.
The only image formats supported for uploading is JPEG and PNG.
.. versionchanged:: 2.0
The edit is no longer in-place, instead the newly edited client user is returned.
Parameters
-----------
password: :class:`str`
The current password for the client's account.
Required for everything except avatar, banner, accent_colour, date_of_birth, and bio.
new_password: :class:`str`
The new password you wish to change to.
email: :class:`str`
The new email you wish to change to.
house: Optional[:class:`HypeSquadHouse`]
The hypesquad house you wish to change to.
Could be ``None`` to leave the current house.
username: :class:`str`
The new username you wish to change to.
avatar: :class:`bytes`
discriminator: :class:`int`
The new discriminator you wish to change to.
Can only be used if you have Nitro.
avatar: Optional[:class:`bytes`]
A :term:`py:bytes-like object` representing the image to upload.
Could be ``None`` to denote no avatar.
banner: :class:`bytes`
A :term:`py:bytes-like object` representing the image to upload.
Could be ``None`` to denote no banner.
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'.
date_of_birth: :class:`datetime.datetime`
Your date of birth. Can only ever be set once.
Raises
------
HTTPException
Editing your profile failed.
InvalidArgument
Wrong image format passed for ``avatar``.
ClientException
Password was not passed when it was required.
`house` field was not a :class:`HypeSquadHouse`.
`date_of_birth` field was not a :class:`datetime.datetime`.
`accent_colo(u)r` parameter was not a :class:`Colour`.
Returns
---------
:class:`ClientUser`
The newly edited client user.
"""
payload: Dict[str, Any] = {}
if username is not MISSING:
payload['username'] = username
args: Dict[str, any] = {}
if any(x is not MISSING for x in ('new_password', 'email', 'username', 'discriminator')):
if password is MISSING:
raise ClientException('Password is required')
args['password'] = password
if avatar is not MISSING:
payload['avatar'] = _bytes_to_base64_data(avatar)
if avatar is not None:
args['avatar'] = _bytes_to_base64_data(avatar)
else:
args['avatar'] = None
if banner is not MISSING:
if banner is not None:
args['banner'] = _bytes_to_base64_data(banner)
else:
args['banner'] = None
if accent_color is not MISSING or accent_colour is not MISSING:
colour = accent_colour if accent_colour is not MISSING else accent_color
if colour is None:
args['accent_color'] = colour
elif not isinstance(colour, Colour):
raise ClientException('`accent_colo(u)r` parameter was not a Colour')
else:
args['accent_color'] = accent_color.value
if email is not MISSING:
args['email'] = email
if username is not MISSING:
args['username'] = username
if discriminator is not MISSING:
args['discriminator'] = discriminator
if new_password is not MISSING:
args['new_password'] = new_password
if bio is not MISSING:
args['bio'] = bio or ''
if date_of_birth is not MISSING:
if not isinstance(date_of_birth, datetime):
raise ClientException('`date_of_birth` parameter was not a datetime')
args['date_of_birth'] = date_of_birth.strftime('%F')
http = self._state.http
if house is not MISSING:
if house is None:
await http.leave_hypesquad_house()
elif not isinstance(house, HypeSquadHouse):
raise ClientException('`house` parameter was not a HypeSquadHouse')
else:
await http.change_hypesquad_house(house.value)
data = await http.edit_profile(**args)
try:
http._token(data['token'])
except KeyError:
pass
data: UserPayload = await self._state.http.edit_profile(payload)
return ClientUser(state=self._state, data=data)
async def fetch_settings(self) -> Settings:
"""|coro|
Retrieves your settings.
.. note::
This method is an API call. For general usage, consider :attr:`settings` instead.
Raises
-------
HTTPException
Retrieving your settings failed.
Returns
--------
:class:`Settings`
The current settings for your account.
"""
data = await self._state.http.get_settings()
return Settings(data=data, state=self._state)
async def edit_settings(self, **kwargs) -> Settings: # TODO: I really wish I didn't have to do this...
"""|coro|
Edits the client user's settings.
Parameters
----------
afk_timeout: :class:`int`
How long (in seconds) the user needs to be AFK until Discord
sends push notifications to your mobile device.
allow_accessibility_detection: :class:`bool`
Whether or not to allow Discord to track screen reader usage.
animate_emojis: :class:`bool`
Whether or not to animate emojis in the chat.
animate_stickers: :class:`StickerAnimationOptions`
Whether or not to animate stickers in the chat.
contact_sync_enabled: :class:`bool`
Whether or not to enable the contact sync on Discord mobile.
convert_emoticons: :class:`bool`
Whether or not to automatically convert emoticons into emojis.
e.g. :-) -> 😃
default_guilds_restricted: :class:`bool`
Whether or not to automatically disable DMs between you and
members of new guilds you join.
detect_platform_accounts: :class:`bool`
Whether or not to automatically detect accounts from services
like Steam and Blizzard when you open the Discord client.
developer_mode: :class:`bool`
Whether or not to enable developer mode.
disable_games_tab: :class:`bool`
Whether or not to disable the showing of the Games tab.
enable_tts_command: :class:`bool`
Whether or not to allow tts messages to be played/sent.
explicit_content_filter: :class:`UserContentFilter`
The filter for explicit content in all messages.
friend_source_flags: :class:`FriendFlags`
Who can add you as a friend.
gif_auto_play: :class:`bool`
Whether or not to automatically play gifs that are in the chat.
guild_positions: List[:class:`abc.Snowflake`]
A list of guilds in order of the guild/guild icons that are on
the left hand side of the UI.
inline_attachment_media: :class:`bool`
Whether or not to display attachments when they are uploaded in chat.
inline_embed_media: :class:`bool`
Whether or not to display videos and images from links posted in chat.
locale: :class:`str`
The :rfc:`3066` language identifier of the locale to use for the language
of the Discord client.
message_display_compact: :class:`bool`
Whether or not to use the compact Discord display mode.
native_phone_integration_enabled: :class:`bool`
Whether or not to enable the new Discord mobile phone number friend
requesting features.
render_embeds: :class:`bool`
Whether or not to render embeds that are sent in the chat.
render_reactions: :class:`bool`
Whether or not to render reactions that are added to messages.
restricted_guilds: List[:class:`abc.Snowflake`]
A list of guilds that you will not receive DMs from.
show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing.
stream_notifications_enabled: :class:`bool`
Unknown.
theme: :class:`Theme`
The theme of the Discord UI.
timezone_offset: :class:`int`
The timezone offset to use.
view_nsfw_guilds: :class:`bool`
Whether or not to show NSFW guilds on iOS.
Raises
-------
HTTPException
Editing the settings failed.
Returns
-------
:class:`.Settings`
The client user's updated settings.
"""
payload = {}
content_filter = kwargs.pop('explicit_content_filter', None)
if content_filter:
payload['explicit_content_filter'] = content_filter.value
animate_stickers = kwargs.pop('animate_stickers', None)
if animate_stickers:
payload['animate_stickers'] = animate_stickers.value
friend_flags = kwargs.pop('friend_source_flags', None)
if friend_flags:
payload['friend_source_flags'] = friend_flags.to_dict()
guild_positions = kwargs.pop('guild_positions', None)
if guild_positions:
guild_positions = [str(x.id) for x in guild_positions]
payload['guild_positions'] = guild_positions
restricted_guilds = kwargs.pop('restricted_guilds', None)
if restricted_guilds:
restricted_guilds = [str(x.id) for x in restricted_guilds]
payload['restricted_guilds'] = restricted_guilds
status = kwargs.pop('status', None)
if status:
payload['status'] = status.value
theme = kwargs.pop('theme', None)
if theme:
payload['theme'] = theme.value
payload.update(kwargs)
state = self._state
data = await state.http.edit_settings(**payload)
state.settings = settings = Settings(data=data, state=self._state)
return settings
class User(BaseUser, discord.abc.Messageable):
class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
"""Represents a Discord user.
.. container:: operations
@ -422,7 +981,7 @@ class User(BaseUser, discord.abc.Messageable):
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
The user's discriminator.
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@ -451,6 +1010,12 @@ class User(BaseUser, discord.abc.Messageable):
self._stored = False
return self
def _get_voice_client_key(self) -> Union[int, str]:
return self._state.user.id, 'self_id'
def _get_voice_state_pair(self) -> Union[int, int]:
return self._state.user.id, self.dm_channel.id
async def _get_channel(self) -> DMChannel:
ch = await self.create_dm()
return ch
@ -464,6 +1029,15 @@ class User(BaseUser, discord.abc.Messageable):
"""
return self._state._get_private_channel_by_user(self.id)
@property
def call(self) -> Optional[Call]:
return getattr(self.dm_channel, 'call', None)
@property
def relationship(self):
"""Optional[:class:`Relationship`]: Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise."""
return self._state.user.get_relationship(self.id)
@property
def mutual_guilds(self) -> List[Guild]:
"""List[:class:`Guild`]: The guilds that the user shares with the client.
@ -476,6 +1050,15 @@ class User(BaseUser, discord.abc.Messageable):
"""
return [guild for guild in self._state._guilds.values() if guild.get_member(self.id)]
async def connect(self, *, ring=True, **kwargs):
channel = await self._get_channel()
call = self.call
if call is not None:
ring = False
await super().connect(_channel=channel, **kwargs)
if ring:
await channel._initial_ring()
async def create_dm(self) -> DMChannel:
"""|coro|
@ -496,3 +1079,114 @@ class User(BaseUser, discord.abc.Messageable):
state = self._state
data: DMChannelPayload = await state.http.start_private_message(self.id)
return state.add_dm_channel(data)
def is_friend(self) -> bool:
""":class:`bool`: Checks if the user is your friend."""
r = self.relationship
if r is None:
return False
return r.type is RelationshipType.friend
def is_blocked(self) -> bool:
""":class:`bool`: Checks if the user is blocked."""
r = self.relationship
if r is None:
return False
return r.type is RelationshipType.blocked
async def block(self) -> None: # TODO: maybe return relationship
"""|coro|
Blocks the user.
Raises
-------
Forbidden
Not allowed to block this user.
HTTPException
Blocking the user failed.
"""
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value, action=RelationshipAction.block)
async def unblock(self) -> None:
"""|coro|
Unblocks the user.
Raises
-------
Forbidden
Not allowed to unblock this user.
HTTPException
Unblocking the user failed.
"""
await self._state.http.remove_relationship(self.id, action=RelationshipAction.unblock)
async def remove_friend(self) -> bool:
"""|coro|
Removes the user as a friend.
Raises
-------
Forbidden
Not allowed to remove this user as a friend.
HTTPException
Removing the user as a friend failed.
"""
await self._state.http.remove_relationship(self.id, action=RelationshipAction.unfriend)
async def send_friend_request(self) -> None: # TODO: maybe return relationship
"""|coro|
Sends the user a friend request.
Raises
-------
Forbidden
Not allowed to send a friend request to the user.
HTTPException
Sending the friend request failed.
"""
await self._state.http.send_friend_request(self.name, self.discriminator)
async def profile(
self, *, with_mutuals: bool = True, fetch_note: bool = True
) -> Profile:
"""|coro|
Gets the user's profile.
Parameters
------------
with_mutuals: :class:`bool`
Whether to fetch mutual guilds and friends.
This fills in :attr:`mutual_guilds` & :attr:`mutual_friends`.
fetch_note: :class:`bool`
Whether to pre-fetch the user's note.
Raises
-------
Forbidden
Not allowed to fetch this profile.
HTTPException
Fetching the profile failed.
Returns
--------
:class:`Profile`
The profile of the user.
"""
user_id = self.id
state = self._state
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals)
if with_mutuals:
data['mutual_friends'] = await self.http.get_mutual_friends(user_id)
profile = Profile(state, data)
if fetch_note:
await profile.note.fetch()
return profile

Loading…
Cancel
Save