diff --git a/discord/settings.py b/discord/settings.py index 721b6a08d..cdb89bc54 100644 --- a/discord/settings.py +++ b/discord/settings.py @@ -27,10 +27,10 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any, Dict, List, Optional, TYPE_CHECKING -from .activity import create_activity +from .activity import create_settings_activity from .enums import FriendFlags, NotificationLevel, Status, StickerAnimationOptions, Theme, UserContentFilter, try_enum from .guild_folder import GuildFolder -from .utils import MISSING, parse_time, utcnow +from .utils import copy_doc, MISSING, parse_time, utcnow if TYPE_CHECKING: from .abc import GuildChannel @@ -134,6 +134,9 @@ class UserSettings: def __repr__(self) -> str: return '' + def _get_guild(self, id: int) -> Optional[Guild]: + return self._state._get_guild(int(id)) + def _update(self, data: Dict[str, Any]) -> None: RAW_VALUES = { 'afk_timeout', @@ -166,6 +169,110 @@ class UserSettings: else: setattr(self, '_' + key, value) + async def edit(self, **kwargs) -> UserSettings: + """|coro| + + Edits the client user's settings. + + .. versionchanged:: 2.0 + The edit is no longer in-place, instead the newly edited settings are returned. + + 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:`.UserSettings` + The client user's updated settings. + """ + return await self._state.user.edit_settings(**kwargs) # type: ignore + + async def fetch_tracking(self) -> Tracking: + """|coro| + + Retrieves your :class:`Tracking` settings. + + Raises + ------ + HTTPException + Retrieving the tracking settings failed. + + Returns + ------- + :class:`Tracking` + The tracking settings. + """ + data = await self._state.http.get_tracking() + return Tracking(state=self._state, data=data) + @property def tracking(self) -> Optional[Tracking]: """Optional[:class:`Tracking`]: Returns your tracking settings if available.""" @@ -179,7 +286,7 @@ class UserSettings: @property def custom_activity(self) -> Optional[CustomActivity]: """Optional[:class:`CustomActivity]: The custom activity you have set.""" - return create_activity(getattr(self, '_custom_status', None)) + return create_settings_activity(data=getattr(self, '_custom_status', None), state=self._state) @property def explicit_content_filter(self) -> UserContentFilter: @@ -221,9 +328,6 @@ class UserSettings: """:class:`Theme`: The theme of the Discord UI.""" return try_enum(Theme, getattr(self, '_theme', 'dark')) # Sane default :) - def _get_guild(self, id: int) -> Optional[Guild]: - return self._state._get_guild(int(id)) - class MuteConfig: def __init__(self, muted: bool, config: Dict[str, str]) -> None: diff --git a/discord/tracking.py b/discord/tracking.py index 4d0a4466e..9b306abd5 100644 --- a/discord/tracking.py +++ b/discord/tracking.py @@ -27,9 +27,14 @@ from __future__ import annotations from base64 import b64encode import json -from typing import Any, Dict, Optional +from typing import Any, Dict, overload, Optional, TYPE_CHECKING -from .types.snowflake import Snowflake +from .utils import MISSING + +if TYPE_CHECKING: + from .enums import ChannelType + from .types.snowflake import Snowflake + from .state import ConnectionState __all__ = ( 'ContextProperties', @@ -46,7 +51,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M __slots__ = ('_data', 'value') def __init__(self, data) -> None: - self._data: Dict[str, any] = data + self._data: Dict[str, Snowflake] = data self.value: str = self._encode_data(data) def _encode_data(self, data) -> str: @@ -139,13 +144,6 @@ class ContextProperties: # Thank you Discord-S.C.U.M } return cls(data) - @classmethod - def _from_accept_invite_page_blank(cls) -> ContextProperties: - data = { - 'location': 'Accept Invite Page' - } - return cls(data) - @classmethod def _from_app(cls) -> ContextProperties: data = { @@ -176,44 +174,63 @@ class ContextProperties: # Thank you Discord-S.C.U.M @classmethod def _from_accept_invite_page( - cls, *, guild_id: Snowflake, channel_id: Snowflake, channel_type: int + cls, + *, + guild_id: Snowflake = MISSING, + channel_id: Snowflake = MISSING, + channel_type: ChannelType = MISSING, ) -> ContextProperties: - data = { + data: Dict[str, Snowflake] = { 'location': 'Accept Invite Page', - 'location_guild_id': str(guild_id), - 'location_channel_id': str(channel_id), - 'location_channel_type': int(channel_type) } + if guild_id is not MISSING: + data['location_guild_id'] = str(guild_id) + if channel_id is not MISSING: + data['location_channel_id'] = str(channel_id) + if channel_type is not MISSING: + data['location_channel_type'] = int(channel_type) return cls(data) @classmethod def _from_join_guild_popup( - cls, *, guild_id: Snowflake, channel_id: Snowflake, channel_type: int + cls, + *, + guild_id: Snowflake = MISSING, + channel_id: Snowflake = MISSING, + channel_type: ChannelType = MISSING, ) -> ContextProperties: - data = { + data: Dict[str, Snowflake] = { 'location': 'Join Guild', - 'location_guild_id': str(guild_id), - 'location_channel_id': str(channel_id), - 'location_channel_type': int(channel_type) } + if guild_id is not MISSING: + data['location_guild_id'] = str(guild_id) + if channel_id is not MISSING: + data['location_channel_id'] = str(channel_id) + if channel_type is not MISSING: + data['location_channel_type'] = int(channel_type) return cls(data) @classmethod def _from_invite_embed( - cls, *, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, channel_type: int + cls, + *, + guild_id: Optional[Snowflake], + channel_id: Snowflake, + message_id: Snowflake, + channel_type: Optional[ChannelType], ) -> ContextProperties: data = { 'location': 'Invite Button Embed', - 'location_guild_id': str(guild_id), + 'location_guild_id': str(guild_id) if guild_id else None, 'location_channel_id': str(channel_id), - 'location_channel_type': int(channel_type), - 'location_message_id': str(message_id) + 'location_channel_type': int(channel_type) if channel_type else None, + 'location_message_id': str(message_id), } return cls(data) @property def location(self) -> Optional[str]: - return self._data.get('location') + return self._data.get('location') # type: ignore @property def guild_id(self) -> Optional[int]: @@ -229,9 +246,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M @property def channel_type(self) -> Optional[int]: - data = self._data.get('location_channel_type') - if data is not None: - return data + return self._data.get('location_channel_type') # type: ignore @property def message_id(self) -> Optional[int]: @@ -243,10 +258,10 @@ class ContextProperties: # Thank you Discord-S.C.U.M return self.value is not None def __str__(self) -> str: - return self._data.get('location', 'None') + return self._data.get('location', 'None') # type: ignore def __repr__(self) -> str: - return ''.format(self) + return f'' def __eq__(self, other) -> bool: return isinstance(other, ContextProperties) and self.value == other.value @@ -262,7 +277,51 @@ class Tracking: ---------- personalization: :class:`bool` Whether you have consented to your data being used for personalization. + usage_statistics: :class:`bool` + Whether you have consented to your data being used for usage statistics. """ - def __init__(self, data: Dict[str, Any]): # TODO: rest of the values + __slots__ = ('_state', 'personalization', 'usage_statistics') + + def __init__(self, *, data: Dict[str, Dict[str, bool]], state: ConnectionState) -> None: + self._state = state + self._update(data) + + def __bool__(self) -> bool: + return any({self.personalization, self.usage_statistics}) + + def _update(self, data: Dict[str, Dict[str, bool]]): self.personalization = data.get('personalization', {}).get('consented', False) + self.usage_statistics = data.get('usage_statistics', {}).get('consented', False) + + @overload + async def edit(self) -> None: + ... + + @overload + async def edit( + self, + *, + personalization: bool = ..., + usage_statistics: bool = ..., + ) -> None: + ... + + async def edit(self, **kwargs) -> None: + """|coro| + + Edits your tracking settings. + + Parameters + ---------- + personalization: :class:`bool` + Whether you have consented to your data being used for personalization. + usage_statistics: :class:`bool` + Whether you have consented to your data being used for usage statistics. + """ + payload = { + 'grant': [k for k, v in kwargs.items() if v is True], + 'revoke': [k for k, v in kwargs.items() if v is False], + } + data = await self._state.http.edit_tracking(payload) + self._update(data) diff --git a/discord/user.py b/discord/user.py index 1e00814bb..88ce6aa68 100644 --- a/discord/user.py +++ b/discord/user.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import Any, Dict, List, Optional, Type, TypeVar, TYPE_CHECKING, Union +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union import discord.abc from .asset import Asset @@ -36,7 +36,7 @@ from .iterators import FakeCommandIterator from .object import Object from .relationship import Relationship from .settings import UserSettings -from .utils import _bytes_to_base64_data, _get_as_snowflake, cached_slot_property, parse_time, snowflake_time, MISSING +from .utils import _bytes_to_base64_data, _get_as_snowflake, cached_slot_property, copy_doc, snowflake_time, MISSING if TYPE_CHECKING: from datetime import datetime @@ -478,6 +478,10 @@ class ClientUser(BaseUser): 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. + nsfw_allowed: :class:`bool` + Specifies if the user should be allowed to access NSFW content. + + .. versionadded:: 2.0 """ __slots__ = ( @@ -491,6 +495,7 @@ class ClientUser(BaseUser): 'note', 'premium', 'bio', + 'nsfw_allowed', ) if TYPE_CHECKING: @@ -503,6 +508,7 @@ class ClientUser(BaseUser): premium: bool premium_type: Optional[PremiumType] bio: Optional[str] + nsfw_allowed: bool def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: super().__init__(state=state, data=data) @@ -525,11 +531,7 @@ class ClientUser(BaseUser): self.premium = data.get('premium', False) self.premium_type = try_enum(PremiumType, data.get('premium_type', None)) self.bio = data.get('bio') or None - - @property - def connected_accounts(self) -> Optional[List[dict]]: - """Optional[List[:class:`dict`]]: Returns a list of all linked accounts for this user if available.""" - return self._state._connected_accounts + self.nsfw_allowed = data.get('nsfw_allowed', False) def get_relationship(self, user_id: int) -> Relationship: """Retrieves the :class:`Relationship` if applicable. @@ -581,7 +583,7 @@ class ClientUser(BaseUser): accent_color: Colour = MISSING, bio: Optional[str] = MISSING, date_of_birth: datetime = MISSING, - ) -> ClientUser: + ) -> ClientUser: """|coro| Edits the current profile of the client. @@ -642,7 +644,7 @@ class ClientUser(BaseUser): :class:`ClientUser` The newly edited client user. """ - args: Dict[str, any] = {} + args: Dict[str, Any] = {} if any(x is not MISSING for x in ('new_password', 'email', 'username', 'discriminator')): if password is MISSING: @@ -700,7 +702,7 @@ class ClientUser(BaseUser): else: await http.change_hypesquad_house(house.value) - data = await http.edit_profile(**args) + data = await http.edit_profile(args) try: http._token(data['token']) except KeyError: @@ -730,90 +732,8 @@ class ClientUser(BaseUser): data = await self._state.http.get_settings() return UserSettings(data=data, state=self._state) + @copy_doc(UserSettings.edit) async def edit_settings(self, **kwargs) -> UserSettings: # TODO: I really wish I didn't have to do this... - """|coro| - - Edits the client user's settings. - - .. versionchanged:: 2.0 - The edit is no longer in-place, instead the newly edited settings are returned. - - 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:`.UserSettings` - The client user's updated settings. - """ payload = {} content_filter = kwargs.pop('explicit_content_filter', None) @@ -842,6 +762,10 @@ class ClientUser(BaseUser): if status: payload['status'] = status.value + custom_activity = kwargs.pop('custom_activity', MISSING) + if custom_activity is not MISSING: + payload['custom_status'] = custom_activity and custom_activity.to_settings_dict() + theme = kwargs.pop('theme', None) if theme: payload['theme'] = theme.value @@ -895,7 +819,7 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): self._stored: bool = False def __repr__(self) -> str: - return f'' + return f'<{self.__class__.__name__} id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot} system={self.system}>' def __del__(self) -> None: try: @@ -910,10 +834,10 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): self._stored = False return self - def _get_voice_client_key(self) -> Union[int, str]: + def _get_voice_client_key(self) -> Tuple[int, str]: return self._state.self_id, 'self_id' - def _get_voice_state_pair(self) -> Union[int, int]: + def _get_voice_state_pair(self) -> Tuple[int, int]: return self._state.self_id, self.dm_channel.id async def _get_channel(self) -> DMChannel: @@ -974,7 +898,7 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): *, limit: Optional[int] = None, command_ids: Optional[List[int]] = [], - **kwargs, + **_, ): """Returns an iterator that allows you to see what user commands are available to use on this user.