You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2462 lines
95 KiB
2462 lines
95 KiB
"""
|
|
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 __future__ import annotations
|
|
|
|
import base64
|
|
import logging
|
|
import struct
|
|
from datetime import datetime, timezone
|
|
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union, overload
|
|
|
|
from discord_protos import PreloadedUserSettings # , FrecencyUserSettings
|
|
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
|
|
from .activity import CustomActivity
|
|
from .colour import Colour
|
|
from .enums import (
|
|
EmojiPickerSection,
|
|
HighlightLevel,
|
|
InboxTab,
|
|
Locale,
|
|
NotificationLevel,
|
|
SpoilerRenderOptions,
|
|
Status,
|
|
StickerAnimationOptions,
|
|
StickerPickerSection,
|
|
Theme,
|
|
UserContentFilter,
|
|
try_enum,
|
|
)
|
|
from .flags import FriendDiscoveryFlags, FriendSourceFlags, HubProgressFlags, OnboardingProgressFlags
|
|
from .object import Object
|
|
from .utils import MISSING, _get_as_snowflake, _ocast, find, parse_time, parse_timestamp, utcnow
|
|
|
|
if TYPE_CHECKING:
|
|
from google.protobuf.message import Message
|
|
from typing_extensions import Self
|
|
|
|
from .abc import GuildChannel, Snowflake
|
|
from .channel import DMChannel, GroupChannel
|
|
from .guild import Guild
|
|
from .state import ConnectionState
|
|
from .types.user import (
|
|
ConsentSettings as ConsentSettingsPayload,
|
|
EmailSettings as EmailSettingsPayload,
|
|
PartialConsentSettings as PartialConsentSettingsPayload,
|
|
UserGuildSettings as UserGuildSettingsPayload,
|
|
ChannelOverride as ChannelOverridePayload,
|
|
MuteConfig as MuteConfigPayload,
|
|
)
|
|
from .user import ClientUser, User
|
|
|
|
PrivateChannel = Union[DMChannel, GroupChannel]
|
|
|
|
__all__ = (
|
|
'UserSettings',
|
|
'GuildFolder',
|
|
'GuildProgress',
|
|
'AudioContext',
|
|
'LegacyUserSettings',
|
|
'MuteConfig',
|
|
'ChannelSettings',
|
|
'GuildSettings',
|
|
'TrackingSettings',
|
|
'EmailSettings',
|
|
)
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
class _ProtoSettings:
|
|
__slots__ = (
|
|
'_state',
|
|
'settings',
|
|
)
|
|
|
|
PROTOBUF_CLS: Type[Message] = MISSING
|
|
settings: Any
|
|
|
|
# I honestly wish I didn't have to vomit properties everywhere like this,
|
|
# but unfortunately it's probably the best way to do it
|
|
# The discord-protos library is maintained seperately, so any changes
|
|
# to the protobufs will have to be reflected here;
|
|
# this is why I'm keeping the `settings` attribute public
|
|
# I love protobufs :blobcatcozystars:
|
|
|
|
def __init__(self, state: ConnectionState, data: str):
|
|
self._state: ConnectionState = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<{self.__class__.__name__}>'
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
if isinstance(other, self.__class__):
|
|
return self.settings == other.settings
|
|
return False
|
|
|
|
def __ne__(self, other: Any) -> bool:
|
|
if isinstance(other, self.__class__):
|
|
return self.settings != other.settings
|
|
return True
|
|
|
|
def _update(self, data: str, *, partial: bool = False):
|
|
if partial:
|
|
self.merge_from_base64(data)
|
|
else:
|
|
self.from_base64(data)
|
|
|
|
@classmethod
|
|
def _copy(cls, self: Self, /) -> Self:
|
|
new = cls.__new__(cls)
|
|
new._state = self._state
|
|
new.settings = cls.PROTOBUF_CLS()
|
|
new.settings.CopyFrom(self.settings)
|
|
return new
|
|
|
|
@overload
|
|
def _get_guild(self, id: int, /, *, always_guild: Literal[True] = ...) -> Guild:
|
|
...
|
|
|
|
@overload
|
|
def _get_guild(self, id: int, /, *, always_guild: Literal[False] = ...) -> Union[Guild, Object]:
|
|
...
|
|
|
|
def _get_guild(self, id: int, /, *, always_guild: bool = False) -> Union[Guild, Object]:
|
|
id = int(id)
|
|
if always_guild:
|
|
return self._state._get_or_create_unavailable_guild(id)
|
|
return self._state._get_guild(id) or Object(id=id)
|
|
|
|
def to_dict(self, *, with_defaults: bool = False) -> Dict[str, Any]:
|
|
return MessageToDict(
|
|
self.settings,
|
|
including_default_value_fields=with_defaults,
|
|
preserving_proto_field_name=True,
|
|
use_integers_for_enums=True,
|
|
)
|
|
|
|
def dict_to_base64(self, data: Dict[str, Any]) -> str:
|
|
message = ParseDict(data, self.PROTOBUF_CLS())
|
|
return base64.b64encode(message.SerializeToString()).decode('ascii')
|
|
|
|
def from_base64(self, data: str):
|
|
self.settings = self.PROTOBUF_CLS().FromString(base64.b64decode(data))
|
|
|
|
def merge_from_base64(self, data: str):
|
|
self.settings.MergeFromString(base64.b64decode(data))
|
|
|
|
def to_base64(self) -> str:
|
|
return base64.b64encode(self.settings.SerializeToString()).decode('ascii')
|
|
|
|
|
|
class UserSettings(_ProtoSettings):
|
|
"""Represents the Discord client settings.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
PROTOBUF_CLS = PreloadedUserSettings
|
|
|
|
# Client versions are supposed to be backwards compatible
|
|
# If the client supports a version newer than the one in data,
|
|
# it does a migration and updates the version in data
|
|
SUPPORTED_CLIENT_VERSION = 17
|
|
SUPPORTED_SERVER_VERSION = 0
|
|
|
|
def __init__(self, *args):
|
|
super().__init__(*args)
|
|
if self.client_version < self.SUPPORTED_CLIENT_VERSION:
|
|
# Migrations are mostly for client state, but we'll throw a debug log anyway
|
|
_log.debug('PreloadedUserSettings client version is outdated, migration needed. Unexpected behaviour may occur.')
|
|
if self.server_version > self.SUPPORTED_SERVER_VERSION:
|
|
# At the time of writing, the server version is not provided (so it's always 0)
|
|
# The client does not use the field at all, so there probably won't be any server-side migrations anytime soon
|
|
_log.debug('PreloadedUserSettings server version is newer than supported. Unexpected behaviour may occur.')
|
|
|
|
@property
|
|
def data_version(self) -> int:
|
|
""":class:`int`: The version of the settings. Increases on every change."""
|
|
return self.settings.versions.data_version
|
|
|
|
@property
|
|
def client_version(self) -> int:
|
|
""":class:`int`: The client version of the settings. Used for client-side data migrations."""
|
|
return self.settings.versions.client_version
|
|
|
|
@property
|
|
def server_version(self) -> int:
|
|
""":class:`int`: The server version of the settings. Used for server-side data migrations."""
|
|
return self.settings.versions.server_version
|
|
|
|
# Inbox Settings
|
|
|
|
@property
|
|
def inbox_tab(self) -> InboxTab:
|
|
""":class:`InboxTab`: The current (last opened) inbox tab."""
|
|
return try_enum(InboxTab, self.settings.inbox.current_tab)
|
|
|
|
@property
|
|
def inbox_tutorial_viewed(self) -> bool:
|
|
""":class:`bool`: Whether the inbox tutorial has been viewed."""
|
|
return self.settings.inbox.viewed_tutorial
|
|
|
|
# Guild Settings
|
|
|
|
@property
|
|
def guild_progress_settings(self) -> List[GuildProgress]:
|
|
"""List[:class:`GuildProgress`]: A list of guild progress settings."""
|
|
state = self._state
|
|
return [
|
|
GuildProgress._from_settings(guild_id, data=settings, state=state)
|
|
for guild_id, settings in self.settings.guilds.guilds.items()
|
|
]
|
|
|
|
# User Content Settings
|
|
|
|
@property
|
|
def dismissed_contents(self) -> Tuple[int, ...]:
|
|
"""Tuple[:class:`int`]: A list of enum values representing dismissable content in the app.
|
|
|
|
.. note::
|
|
|
|
For now, this just returns the raw values without converting to a proper enum,
|
|
as the enum values change too often to be viably maintained.
|
|
"""
|
|
contents = self.settings.user_content.dismissed_contents
|
|
return struct.unpack(f'>{len(contents)}B', contents)
|
|
|
|
@property
|
|
def last_dismissed_promotion_start_date(self) -> Optional[datetime]:
|
|
"""Optional[:class:`datetime.datetime`]: The date the last dismissed promotion started."""
|
|
return parse_time(self.settings.user_content.last_dismissed_outbound_promotion_start_date.value or None)
|
|
|
|
@property
|
|
def nitro_basic_modal_dismissed_at(self) -> Optional[datetime]:
|
|
"""Optional[:class:`datetime.datetime`]: The date the Nitro Basic modal was dismissed."""
|
|
return (
|
|
self.settings.user_content.premium_tier_0_modal_dismissed_at.ToDatetime(tzinfo=timezone.utc)
|
|
if self.settings.user_content.HasField('premium_tier_0_modal_dismissed_at')
|
|
else None
|
|
)
|
|
|
|
# Voice and Video Settings
|
|
|
|
# TODO: Video filters
|
|
|
|
# @property
|
|
# def video_filter_background_blur(self) -> bool:
|
|
# return self.settings.voice_and_video.blur.use_blur
|
|
|
|
@property
|
|
def always_preview_video(self) -> bool:
|
|
"""Whether to always show the preview modal when the user turns on their camera."""
|
|
return self.settings.voice_and_video.always_preview_video.value
|
|
|
|
@property
|
|
def afk_timeout(self) -> int:
|
|
""":class:`int`: How long (in seconds) the user needs to be AFK until Discord sends push notifications to mobile devices (30-600)."""
|
|
return self.settings.voice_and_video.afk_timeout.value or 600
|
|
|
|
@property
|
|
def stream_notifications_enabled(self) -> bool:
|
|
""":class:`bool`: Whether stream notifications for friends will be received."""
|
|
return (
|
|
self.settings.voice_and_video.stream_notifications_enabled.value
|
|
if self.settings.voice_and_video.HasField('stream_notifications_enabled')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def native_phone_integration_enabled(self) -> bool:
|
|
""":class:`bool`: Whether to enable the Discord mobile Callkit."""
|
|
return (
|
|
self.settings.voice_and_video.native_phone_integration_enabled.value
|
|
if self.settings.voice_and_video.HasField('native_phone_integration_enabled')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def soundboard_volume(self) -> float:
|
|
""":class:`float`: The volume of the soundboard (0-100)."""
|
|
return (
|
|
self.settings.voice_and_video.soundboard_settings.volume
|
|
if self.settings.voice_and_video.HasField('soundboard_settings')
|
|
else 100.0
|
|
)
|
|
|
|
# Text and Images Settings
|
|
|
|
@property
|
|
def diversity_surrogate(self) -> Optional[str]:
|
|
"""Optional[:class:`str`]: The unicode character used as the diversity surrogate for supported emojis (i.e. emoji skin tones, ``🏻``)."""
|
|
return self.settings.text_and_images.diversity_surrogate.value or None
|
|
|
|
@property
|
|
def use_thread_sidebar(self) -> bool:
|
|
""":class:`bool`: Whether to open threads in split view."""
|
|
return (
|
|
self.settings.text_and_images.use_thread_sidebar.value
|
|
if self.settings.text_and_images.HasField('use_thread_sidebar')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def render_spoilers(self) -> SpoilerRenderOptions:
|
|
""":class:`SpoilerRenderOptions`: When to show spoiler content."""
|
|
return try_enum(SpoilerRenderOptions, self.settings.text_and_images.render_spoilers.value or 'ON_CLICK')
|
|
|
|
@property
|
|
def collapsed_emoji_picker_sections(self) -> Tuple[Union[EmojiPickerSection, Guild], ...]:
|
|
"""Tuple[Union[:class:`EmojiPickerSection`, :class:`Guild`]]: A list of emoji picker sections (including guild IDs) that are collapsed."""
|
|
return tuple(
|
|
self._get_guild(section, always_guild=True) if section.isdigit() else try_enum(EmojiPickerSection, section)
|
|
for section in self.settings.text_and_images.emoji_picker_collapsed_sections
|
|
)
|
|
|
|
@property
|
|
def collapsed_sticker_picker_sections(self) -> Tuple[Union[StickerPickerSection, Guild, Object], ...]:
|
|
"""Tuple[Union[:class:`StickerPickerSection`, :class:`Guild`, :class:`Object`]]: A list of sticker picker sections (including guild and sticker pack IDs) that are collapsed."""
|
|
return tuple(
|
|
self._get_guild(section, always_guild=False) if section.isdigit() else try_enum(StickerPickerSection, section)
|
|
for section in self.settings.text_and_images.sticker_picker_collapsed_sections
|
|
)
|
|
|
|
@property
|
|
def view_image_descriptions(self) -> bool:
|
|
""":class:`bool`: Whether to display the alt text of attachments."""
|
|
return self.settings.text_and_images.view_image_descriptions.value
|
|
|
|
@property
|
|
def show_command_suggestions(self) -> bool:
|
|
""":class:`bool`: Whether to show application command suggestions in-chat."""
|
|
return (
|
|
self.settings.text_and_images.show_command_suggestions.value
|
|
if self.settings.text_and_images.HasField('show_command_suggestions')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def inline_attachment_media(self) -> bool:
|
|
""":class:`bool`: Whether to display attachments when they are uploaded in chat."""
|
|
return (
|
|
self.settings.text_and_images.inline_attachment_media.value
|
|
if self.settings.text_and_images.HasField('inline_attachment_media')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def inline_embed_media(self) -> bool:
|
|
""":class:`bool`: Whether to display videos and images from links posted in chat."""
|
|
return (
|
|
self.settings.text_and_images.inline_embed_media.value
|
|
if self.settings.text_and_images.HasField('inline_embed_media')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def gif_auto_play(self) -> bool:
|
|
""":class:`bool`: Whether to automatically play GIFs that are in the chat.."""
|
|
return (
|
|
self.settings.text_and_images.gif_auto_play.value
|
|
if self.settings.text_and_images.HasField('gif_auto_play')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def render_embeds(self) -> bool:
|
|
""":class:`bool`: Whether to render embeds that are sent in the chat."""
|
|
return (
|
|
self.settings.text_and_images.render_embeds.value
|
|
if self.settings.text_and_images.HasField('render_embeds')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def render_reactions(self) -> bool:
|
|
""":class:`bool`: Whether to render reactions that are added to messages."""
|
|
return (
|
|
self.settings.text_and_images.render_reactions.value
|
|
if self.settings.text_and_images.HasField('render_reactions')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def animate_emojis(self) -> bool:
|
|
""":class:`bool`: Whether to animate emojis in the chat."""
|
|
return (
|
|
self.settings.text_and_images.animate_emoji.value
|
|
if self.settings.text_and_images.HasField('animate_emoji')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def animate_stickers(self) -> StickerAnimationOptions:
|
|
""":class:`StickerAnimationOptions`: Whether to animate stickers in the chat."""
|
|
return try_enum(StickerAnimationOptions, self.settings.text_and_images.animate_stickers.value)
|
|
|
|
@property
|
|
def enable_tts_command(self) -> bool:
|
|
""":class:`bool`: Whether to allow TTS messages to be played/sent."""
|
|
return (
|
|
self.settings.text_and_images.enable_tts_command.value
|
|
if self.settings.text_and_images.HasField('enable_tts_command')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def message_display_compact(self) -> bool:
|
|
""":class:`bool`: Whether to use the compact Discord display mode."""
|
|
return self.settings.text_and_images.message_display_compact.value
|
|
|
|
@property
|
|
def explicit_content_filter(self) -> UserContentFilter:
|
|
""":class:`UserContentFilter`: The filter for explicit content in all messages."""
|
|
return try_enum(
|
|
UserContentFilter,
|
|
self.settings.text_and_images.explicit_content_filter.value
|
|
if self.settings.text_and_images.HasField('explicit_content_filter')
|
|
else 1,
|
|
)
|
|
|
|
@property
|
|
def view_nsfw_guilds(self) -> bool:
|
|
""":class:`bool`: Whether to show NSFW guilds on iOS."""
|
|
return self.settings.text_and_images.view_nsfw_guilds.value
|
|
|
|
@property
|
|
def convert_emoticons(self) -> bool:
|
|
r""":class:`bool`: Whether to automatically convert emoticons into emojis (e.g. ``:)`` -> 😃)."""
|
|
return (
|
|
self.settings.text_and_images.convert_emoticons.value
|
|
if self.settings.text_and_images.HasField('convert_emoticons')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def show_expression_suggestions(self) -> bool:
|
|
""":class:`bool`: Whether to show expression (emoji/sticker/soundboard) suggestions in-chat."""
|
|
return (
|
|
self.settings.text_and_images.expression_suggestions_enabled.value
|
|
if self.settings.text_and_images.HasField('expression_suggestions_enabled')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def view_nsfw_commands(self) -> bool:
|
|
""":class:`bool`: Whether to show NSFW application commands in DMs."""
|
|
return self.settings.text_and_images.view_nsfw_commands.value
|
|
|
|
@property
|
|
def use_legacy_chat_input(self) -> bool:
|
|
""":class:`bool`: Whether to use the legacy chat input over the new rich input."""
|
|
return self.settings.text_and_images.use_legacy_chat_input.value
|
|
|
|
# Notifications Settings
|
|
|
|
@property
|
|
def in_app_notifications(self) -> bool:
|
|
""":class:`bool`: Whether to show notifications directly in the app."""
|
|
return (
|
|
self.settings.notifications.show_in_app_notifications.value
|
|
if self.settings.notifications.HasField('show_in_app_notifications')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def send_stream_notifications(self) -> bool:
|
|
""":class:`bool`: Whether to send notifications to friends when using the go live feature."""
|
|
return self.settings.notifications.notify_friends_on_go_live.value
|
|
|
|
@property
|
|
def notification_center_acked_before_id(self) -> int:
|
|
""":class:`int`: The ID of the last notification that was acknowledged in the notification center."""
|
|
return self.settings.notifications.notification_center_acked_before_id
|
|
|
|
# Privacy Settings
|
|
|
|
@property
|
|
def allow_activity_friend_joins(self) -> bool:
|
|
""":class:`bool`: Whether to allow friends to join your activity without sending a request."""
|
|
return (
|
|
self.settings.privacy.allow_activity_party_privacy_friends.value
|
|
if self.settings.privacy.HasField('allow_activity_party_privacy_friends')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def allow_activity_voice_channel_joins(self) -> bool:
|
|
""":class:`bool`: Whether to allow people in the same voice channel as you to join your activity without sending a request. Does not apply to Community guilds."""
|
|
return (
|
|
self.settings.privacy.allow_activity_party_privacy_voice_channel.value
|
|
if self.settings.privacy.HasField('allow_activity_party_privacy_voice_channel')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that you will not receive DMs from."""
|
|
return list(map(self._get_guild, self.settings.privacy.restricted_guild_ids))
|
|
|
|
@property
|
|
def default_guilds_restricted(self) -> bool:
|
|
""":class:`bool`: Whether to automatically disable DMs between you and members of new guilds you join."""
|
|
return self.settings.privacy.default_guilds_restricted
|
|
|
|
@property
|
|
def allow_accessibility_detection(self) -> bool:
|
|
""":class:`bool`: Whether to allow Discord to track screen reader usage."""
|
|
return self.settings.privacy.allow_accessibility_detection
|
|
|
|
@property
|
|
def detect_platform_accounts(self) -> bool:
|
|
""":class:`bool`: Whether to automatically detect accounts from services like Steam and Blizzard when you open the Discord client."""
|
|
return (
|
|
self.settings.privacy.detect_platform_accounts.value
|
|
if self.settings.privacy.HasField('detect_platform_accounts')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def passwordless(self) -> bool:
|
|
""":class:`bool`: Whether to enable passwordless login."""
|
|
return self.settings.privacy.passwordless.value if self.settings.privacy.HasField('passwordless') else True
|
|
|
|
@property
|
|
def contact_sync_enabled(self) -> bool:
|
|
""":class:`bool`: Whether to enable the contact sync on Discord mobile."""
|
|
return self.settings.privacy.contact_sync_enabled.value
|
|
|
|
@property
|
|
def friend_source_flags(self) -> FriendSourceFlags:
|
|
""":class:`FriendSourceFlags`: Who can add you as a friend."""
|
|
return (
|
|
FriendSourceFlags._from_value(self.settings.privacy.friend_source_flags.value)
|
|
if self.settings.privacy.HasField('friend_source_flags')
|
|
else FriendSourceFlags.all()
|
|
)
|
|
|
|
@property
|
|
def friend_discovery_flags(self) -> FriendDiscoveryFlags:
|
|
""":class:`FriendDiscoveryFlags`: How you get recommended friends."""
|
|
return FriendDiscoveryFlags._from_value(self.settings.privacy.friend_discovery_flags.value)
|
|
|
|
@property
|
|
def activity_restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that your current activity will not be shown in."""
|
|
return list(map(self._get_guild, self.settings.privacy.activity_restricted_guild_ids))
|
|
|
|
@property
|
|
def default_guilds_activity_restricted(self) -> bool:
|
|
""":class:`bool`: Whether to automatically disable showing your current activity in new large (over 200 member) guilds you join."""
|
|
return self.settings.privacy.default_guilds_activity_restricted
|
|
|
|
@property
|
|
def activity_joining_restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that will not be able to join your current activity."""
|
|
return list(map(self._get_guild, self.settings.privacy.activity_joining_restricted_guild_ids))
|
|
|
|
@property
|
|
def message_request_restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds whose originating DMs will not be filtered into your message requests."""
|
|
return list(map(self._get_guild, self.settings.privacy.message_request_restricted_guild_ids))
|
|
|
|
@property
|
|
def default_message_request_restricted(self) -> bool:
|
|
""":class:`bool`: Whether to automatically disable the message request system in new guilds you join."""
|
|
return self.settings.privacy.default_message_request_restricted.value
|
|
|
|
@property
|
|
def drops(self) -> bool:
|
|
""":class:`bool`: Whether the Discord drops feature is enabled."""
|
|
return not self.settings.privacy.drops_opted_out.value
|
|
|
|
@property
|
|
def non_spam_retraining(self) -> Optional[bool]:
|
|
"""Optional[:class:`bool`]: Whether to help improve Discord spam models when marking messages as non-spam; staff only."""
|
|
return (
|
|
self.settings.privacy.non_spam_retraining_opt_in.value
|
|
if self.settings.privacy.HasField('non_spam_retraining_opt_in')
|
|
else None
|
|
)
|
|
|
|
# Debug Settings
|
|
|
|
@property
|
|
def rtc_panel_show_voice_states(self) -> bool:
|
|
""":class:`bool`: Whether to show voice states in the RTC panel."""
|
|
return self.settings.debug.rtc_panel_show_voice_states.value
|
|
|
|
# Game Library Settings
|
|
|
|
@property
|
|
def install_shortcut_desktop(self) -> bool:
|
|
""":class:`bool`: Whether to install a desktop shortcut for games."""
|
|
return self.settings.game_library.install_shortcut_desktop.value
|
|
|
|
@property
|
|
def install_shortcut_start_menu(self) -> bool:
|
|
""":class:`bool`: Whether to install a start menu shortcut for games."""
|
|
return (
|
|
self.settings.game_library.install_shortcut_start_menu.value
|
|
if self.settings.game_library.HasField('install_shortcut_start_menu')
|
|
else True
|
|
)
|
|
|
|
@property
|
|
def disable_games_tab(self) -> bool:
|
|
""":class:`bool`: Whether to disable the showing of the Games tab."""
|
|
return self.settings.game_library.disable_games_tab.value
|
|
|
|
# Status Settings
|
|
|
|
@property
|
|
def status(self) -> Status:
|
|
""":class:`Status`: The configured status."""
|
|
return try_enum(Status, self.settings.status.status.value or 'unknown')
|
|
|
|
@property
|
|
def custom_activity(self) -> Optional[CustomActivity]:
|
|
""":class:`CustomActivity`: The set custom activity."""
|
|
return (
|
|
CustomActivity._from_settings(data=self.settings.status.custom_status, state=self._state)
|
|
if self.settings.status.HasField('custom_status')
|
|
else None
|
|
)
|
|
|
|
@property
|
|
def show_current_game(self) -> bool:
|
|
""":class:`bool`: Whether to show the current game."""
|
|
return self.settings.status.show_current_game.value if self.settings.status.HasField('show_current_game') else True
|
|
|
|
# Localization Settings
|
|
|
|
@property
|
|
def locale(self) -> Locale:
|
|
""":class:`Locale`: The :rfc:`3066` language identifier of the locale to use for the language of the Discord client."""
|
|
return try_enum(Locale, self.settings.localization.locale.value or 'en-US')
|
|
|
|
@property
|
|
def timezone_offset(self) -> int:
|
|
""":class:`int`: The timezone offset from UTC to use (in minutes)."""
|
|
return self.settings.localization.timezone_offset.value
|
|
|
|
# Appearance Settings
|
|
|
|
@property
|
|
def theme(self) -> Theme:
|
|
""":class:`Theme`: The overall theme of the Discord UI."""
|
|
return Theme.from_int(self.settings.appearance.theme)
|
|
|
|
@property
|
|
def client_theme(self) -> Optional[Tuple[int, int, float]]:
|
|
"""Optional[Tuple[:class:`int`, :class:`int`, :class:`float`]]: The client theme settings, in order of primary color, gradient preset, and gradient angle."""
|
|
return (
|
|
(
|
|
self.settings.appearance.client_theme_settings.primary_color.value,
|
|
self.settings.appearance.client_theme_settings.background_gradient_preset_id.value,
|
|
self.settings.appearance.client_theme_settings.background_gradient_angle.value,
|
|
)
|
|
if self.settings.appearance.HasField('client_theme_settings')
|
|
else None
|
|
)
|
|
|
|
@property
|
|
def developer_mode(self) -> bool:
|
|
""":class:`bool`: Whether to enable developer mode."""
|
|
return self.settings.appearance.developer_mode
|
|
|
|
@property
|
|
def disable_mobile_redesign(self) -> bool:
|
|
""":class:`bool`: Whether to opt-out of the mobile redesign."""
|
|
return self.settings.appearance.mobile_redesign_disabled
|
|
|
|
# Guild Folder Settings
|
|
|
|
@property
|
|
def guild_folders(self) -> List[GuildFolder]:
|
|
"""List[:class:`GuildFolder`]: A list of guild folders."""
|
|
state = self._state
|
|
return [GuildFolder._from_settings(data=folder, state=state) for folder in self.settings.guild_folders.folders]
|
|
|
|
@property
|
|
def guild_positions(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI."""
|
|
return list(map(self._get_guild, self.settings.guild_folders.guild_positions))
|
|
|
|
# Favorites Settings
|
|
|
|
# TODO: Favorites
|
|
|
|
# Audio Settings
|
|
|
|
@property
|
|
def user_audio_settings(self) -> List[AudioContext]:
|
|
"""List[:class:`AudioContext`]: A list of audio context settings for users."""
|
|
state = self._state
|
|
return [
|
|
AudioContext._from_settings(user_id, data=data, state=state)
|
|
for user_id, data in self.settings.audio_context_settings.user.items()
|
|
]
|
|
|
|
@property
|
|
def stream_audio_settings(self) -> List[AudioContext]:
|
|
"""List[:class:`AudioContext`]: A list of audio context settings for streams."""
|
|
state = self._state
|
|
return [
|
|
AudioContext._from_settings(stream_id, data=data, state=state)
|
|
for stream_id, data in self.settings.audio_context_settings.stream.items()
|
|
]
|
|
|
|
# Communities Settings
|
|
|
|
@property
|
|
def home_auto_navigation(self) -> bool:
|
|
""":class:`bool`: Whether to automatically redirect to guild home for guilds that have not been accessed in a while."""
|
|
return not self.settings.communities.disable_home_auto_nav.value
|
|
|
|
@overload
|
|
async def edit(self) -> Self:
|
|
...
|
|
|
|
@overload
|
|
async def edit(
|
|
self,
|
|
*,
|
|
require_version: Union[bool, int] = False,
|
|
client_version: int = ...,
|
|
inbox_tab: InboxTab = ...,
|
|
inbox_tutorial_viewed: bool = ...,
|
|
guild_progress_settings: Sequence[GuildProgress] = ...,
|
|
dismissed_contents: Sequence[int] = ...,
|
|
last_dismissed_promotion_start_date: datetime = ...,
|
|
nitro_basic_modal_dismissed_at: datetime = ...,
|
|
soundboard_volume: float = ...,
|
|
afk_timeout: int = ...,
|
|
always_preview_video: bool = ...,
|
|
native_phone_integration_enabled: bool = ...,
|
|
stream_notifications_enabled: bool = ...,
|
|
diversity_surrogate: Optional[str] = ...,
|
|
render_spoilers: SpoilerRenderOptions = ...,
|
|
collapsed_emoji_picker_sections: Sequence[Union[EmojiPickerSection, Snowflake]] = ...,
|
|
collapsed_sticker_picker_sections: Sequence[Union[StickerPickerSection, Snowflake]] = ...,
|
|
animate_emojis: bool = ...,
|
|
animate_stickers: StickerAnimationOptions = ...,
|
|
explicit_content_filter: UserContentFilter = ...,
|
|
show_expression_suggestions: bool = ...,
|
|
use_thread_sidebar: bool = ...,
|
|
view_image_descriptions: bool = ...,
|
|
show_command_suggestions: bool = ...,
|
|
inline_attachment_media: bool = ...,
|
|
inline_embed_media: bool = ...,
|
|
gif_auto_play: bool = ...,
|
|
render_embeds: bool = ...,
|
|
render_reactions: bool = ...,
|
|
enable_tts_command: bool = ...,
|
|
message_display_compact: bool = ...,
|
|
view_nsfw_guilds: bool = ...,
|
|
convert_emoticons: bool = ...,
|
|
view_nsfw_commands: bool = ...,
|
|
use_legacy_chat_input: bool = ...,
|
|
in_app_notifications: bool = ...,
|
|
send_stream_notifications: bool = ...,
|
|
notification_center_acked_before_id: int = ...,
|
|
allow_activity_friend_joins: bool = ...,
|
|
allow_activity_voice_channel_joins: bool = ...,
|
|
friend_source_flags: FriendSourceFlags = ...,
|
|
friend_discovery_flags: FriendDiscoveryFlags = ...,
|
|
drops: bool = ...,
|
|
non_spam_retraining: Optional[bool] = ...,
|
|
restricted_guilds: Sequence[Snowflake] = ...,
|
|
default_guilds_restricted: bool = ...,
|
|
allow_accessibility_detection: bool = ...,
|
|
detect_platform_accounts: bool = ...,
|
|
passwordless: bool = ...,
|
|
contact_sync_enabled: bool = ...,
|
|
activity_restricted_guilds: Sequence[Snowflake] = ...,
|
|
default_guilds_activity_restricted: bool = ...,
|
|
activity_joining_restricted_guilds: Sequence[Snowflake] = ...,
|
|
message_request_restricted_guilds: Sequence[Snowflake] = ...,
|
|
default_message_request_restricted: bool = ...,
|
|
rtc_panel_show_voice_states: bool = ...,
|
|
install_shortcut_desktop: bool = ...,
|
|
install_shortcut_start_menu: bool = ...,
|
|
disable_games_tab: bool = ...,
|
|
status: Status = ...,
|
|
custom_activity: Optional[CustomActivity] = ...,
|
|
show_current_game: bool = ...,
|
|
locale: Locale = ...,
|
|
timezone_offset: int = ...,
|
|
theme: Theme = ...,
|
|
client_theme: Optional[Tuple[int, int, float]] = ...,
|
|
disable_mobile_redesign: bool = ...,
|
|
developer_mode: bool = ...,
|
|
guild_folders: Sequence[GuildFolder] = ...,
|
|
guild_positions: Sequence[Snowflake] = ...,
|
|
user_audio_settings: Collection[AudioContext] = ...,
|
|
stream_audio_settings: Collection[AudioContext] = ...,
|
|
home_auto_navigation: bool = ...,
|
|
) -> Self:
|
|
...
|
|
|
|
async def edit(self, *, require_version: Union[bool, int] = False, **kwargs: Any) -> Self:
|
|
r"""|coro|
|
|
|
|
Edits the current user's settings.
|
|
|
|
.. note::
|
|
|
|
Settings subsections are not idempotently updated. This means if you change one setting in a subsection\* on an outdated
|
|
instance of :class:`UserSettings` then the other settings in that subsection\* will be reset to the value of the instance.
|
|
|
|
When operating on the cached user settings (i.e. :attr:`Client.settings`), this should not be an issue. However, if you
|
|
are operating on a fetched instance, consider using the ``require_version`` parameter to ensure you don't overwrite
|
|
newer settings.
|
|
|
|
Any field may be explicitly set to ``MISSING`` to reset it to the default value.
|
|
|
|
\* A subsection is a group of settings that are stored in the same top-level protobuf message.
|
|
Examples include Privacy, Text and Images, Voice and Video, etc.
|
|
|
|
.. note::
|
|
|
|
This method is ratelimited heavily. Updates should be batched together and sent at intervals.
|
|
|
|
Infrequent actions do not need a delay. Frequent actions should be delayed by 10 seconds and batched.
|
|
Automated actions (such as migrations or frecency updates) should be delayed by 30 seconds and batched.
|
|
Daily actions (things that change often and are not meaningful, such as emoji frencency) should be delayed by 1 day and batched.
|
|
|
|
Parameters
|
|
----------
|
|
require_version: Union[:class:`bool`, :class:`int`]
|
|
Whether to require the current version of the settings to be the same as the provided version.
|
|
If this is ``True`` then the current version is used.
|
|
\*\*kwargs
|
|
The settings to edit. Refer to the :class:`UserSettings` properties for the valid fields. Unknown fields are ignored.
|
|
|
|
Raises
|
|
------
|
|
HTTPException
|
|
Editing the settings failed.
|
|
TypeError
|
|
At least one setting is required to edit.
|
|
|
|
Returns
|
|
-------
|
|
:class:`UserSettings`
|
|
The edited settings. Note that this is a new instance and not the same as the cached instance as mentioned above.
|
|
"""
|
|
# As noted above, entire sections MUST be sent, or they will be reset to default values
|
|
# Conversely, we want to omit fields that the user requests to be set to default (by explicitly passing MISSING)
|
|
# For this, we then remove fields set to MISSING from the payload in the payload construction at the end
|
|
|
|
if not kwargs:
|
|
raise TypeError('edit() missing at least 1 required keyword-only argument')
|
|
|
|
# Only client_version should ever really be sent
|
|
versions = {}
|
|
for field in ('data_version', 'client_version', 'server_version'):
|
|
if field in kwargs:
|
|
versions[field] = kwargs.pop(field)
|
|
|
|
inbox = {}
|
|
if 'inbox_tab' in kwargs:
|
|
inbox['current_tab'] = _ocast(kwargs.pop('inbox_tab'), int)
|
|
if 'inbox_tutorial_viewed' in kwargs:
|
|
inbox['viewed_tutorial'] = kwargs.pop('inbox_tutorial_viewed')
|
|
|
|
guilds = {}
|
|
if 'guild_progress_settings' in kwargs and kwargs['guild_progress_settings'] is not MISSING:
|
|
guilds['guilds'] = (
|
|
{guild.guild_id: guild.to_dict() for guild in kwargs.pop('guild_progress_settings')}
|
|
if kwargs['guild_progress_settings'] is not MISSING
|
|
else MISSING
|
|
)
|
|
|
|
user_content = {}
|
|
if 'dismissed_contents' in kwargs:
|
|
contents = kwargs.pop('dismissed_contents')
|
|
user_content['dismissed_contents'] = (
|
|
struct.pack(f'>{len(contents)}B', *contents) if contents is not MISSING else MISSING
|
|
)
|
|
if 'last_dismissed_promotion_start_date' in kwargs:
|
|
user_content['last_dismissed_outbound_promotion_start_date'] = (
|
|
kwargs.pop('last_dismissed_promotion_start_date').isoformat()
|
|
if kwargs['last_dismissed_promotion_start_date'] is not MISSING
|
|
else MISSING
|
|
)
|
|
if 'nitro_basic_modal_dismissed_at' in kwargs:
|
|
user_content['premium_tier_0_modal_dismissed_at'] = (
|
|
kwargs.pop('nitro_basic_modal_dismissed_at').isoformat()
|
|
if kwargs['nitro_basic_modal_dismissed_at'] is not MISSING
|
|
else MISSING
|
|
)
|
|
|
|
voice_and_video = {}
|
|
if 'soundboard_volume' in kwargs:
|
|
voice_and_video['soundboard_settings'] = (
|
|
{'volume': kwargs.pop('soundboard_volume')} if kwargs['soundboard_volume'] is not MISSING else {}
|
|
)
|
|
for field in (
|
|
'afk_timeout',
|
|
'always_preview_video',
|
|
'native_phone_integration_enabled',
|
|
'stream_notifications_enabled',
|
|
):
|
|
if field in kwargs:
|
|
voice_and_video[field] = kwargs.pop(field)
|
|
|
|
text_and_images = {}
|
|
if 'diversity_surrogate' in kwargs:
|
|
text_and_images['diversity_surrogate'] = (
|
|
kwargs.pop('diversity_surrogate') or '' if kwargs['diversity_surrogate'] is not MISSING else MISSING
|
|
)
|
|
if 'render_spoilers' in kwargs:
|
|
text_and_images['render_spoilers'] = _ocast(kwargs.pop('render_spoilers'), str)
|
|
if 'collapsed_emoji_picker_sections' in kwargs:
|
|
text_and_images['emoji_picker_collapsed_sections'] = (
|
|
[str(getattr(x, 'id', x)) for x in kwargs.pop('collapsed_emoji_picker_sections')]
|
|
if kwargs['collapsed_emoji_picker_sections'] is not MISSING
|
|
else MISSING
|
|
)
|
|
if 'collapsed_sticker_picker_sections' in kwargs:
|
|
text_and_images['sticker_picker_collapsed_sections'] = (
|
|
[str(getattr(x, 'id', x)) for x in kwargs.pop('collapsed_sticker_picker_sections')]
|
|
if kwargs['collapsed_sticker_picker_sections'] is not MISSING
|
|
else MISSING
|
|
)
|
|
if 'animate_emojis' in kwargs:
|
|
text_and_images['animate_emoji'] = kwargs.pop('animate_emojis')
|
|
if 'animate_stickers' in kwargs:
|
|
text_and_images['animate_stickers'] = _ocast(kwargs.pop('animate_stickers'), int)
|
|
if 'explicit_content_filter' in kwargs:
|
|
text_and_images['explicit_content_filter'] = _ocast(kwargs.pop('explicit_content_filter'), int)
|
|
if 'show_expression_suggestions' in kwargs:
|
|
text_and_images['expression_suggestions_enabled'] = kwargs.pop('show_expression_suggestions')
|
|
for field in (
|
|
'use_thread_sidebar',
|
|
'view_image_descriptions',
|
|
'show_command_suggestions',
|
|
'inline_attachment_media',
|
|
'inline_embed_media',
|
|
'gif_auto_play',
|
|
'render_embeds',
|
|
'render_reactions',
|
|
'enable_tts_command',
|
|
'message_display_compact',
|
|
'view_nsfw_guilds',
|
|
'convert_emoticons',
|
|
'view_nsfw_commands',
|
|
'use_legacy_chat_input',
|
|
'use_rich_chat_input',
|
|
):
|
|
if field in kwargs:
|
|
text_and_images[field] = kwargs.pop(field)
|
|
|
|
notifications = {}
|
|
if 'in_app_notifications' in kwargs:
|
|
notifications['show_in_app_notifications'] = kwargs.pop('in_app_notifications')
|
|
if 'send_stream_notifications' in kwargs:
|
|
notifications['notify_friends_on_go_live'] = kwargs.pop('send_stream_notifications')
|
|
for field in ('notification_center_acked_before_id',):
|
|
if field in kwargs:
|
|
notifications[field] = kwargs.pop(field)
|
|
|
|
privacy = {}
|
|
if 'allow_activity_friend_joins' in kwargs:
|
|
privacy['allow_activity_party_privacy_friends'] = kwargs.pop('allow_activity_friend_joins')
|
|
if 'allow_activity_voice_channel_joins' in kwargs:
|
|
privacy['allow_activity_party_privacy_voice_channel'] = kwargs.pop('allow_activity_voice_channel_joins')
|
|
if 'friend_source_flags' in kwargs:
|
|
privacy['friend_source_flags'] = (
|
|
kwargs.pop('friend_source_flags').value if kwargs['friend_source_flags'] is not MISSING else MISSING
|
|
)
|
|
if 'friend_discovery_flags' in kwargs:
|
|
privacy['friend_discovery_flags'] = (
|
|
kwargs.pop('friend_discovery_flags').value if kwargs['friend_discovery_flags'] is not MISSING else MISSING
|
|
)
|
|
if 'drops' in kwargs:
|
|
privacy['drops_opted_out'] = not kwargs.pop('drops') if kwargs['drops'] is not MISSING else MISSING
|
|
if 'non_spam_retraining' in kwargs:
|
|
privacy['non_spam_retraining_opt_in'] = (
|
|
kwargs.pop('non_spam_retraining') if kwargs['non_spam_retraining'] not in {None, MISSING} else MISSING
|
|
)
|
|
for field in (
|
|
'restricted_guilds',
|
|
'default_guilds_restricted',
|
|
'allow_accessibility_detection',
|
|
'detect_platform_accounts',
|
|
'passwordless',
|
|
'contact_sync_enabled',
|
|
'activity_restricted_guilds',
|
|
'default_guilds_activity_restricted',
|
|
'activity_joining_restricted_guilds',
|
|
'message_request_restricted_guilds',
|
|
'default_message_request_restricted',
|
|
):
|
|
if field in kwargs:
|
|
if field.endswith('_guilds'):
|
|
privacy[field.replace('_guilds', '_guild_ids')] = [g.id for g in kwargs.pop(field)]
|
|
else:
|
|
privacy[field] = kwargs.pop(field)
|
|
|
|
debug = {}
|
|
for field in ('rtc_panel_show_voice_states',):
|
|
if field in kwargs:
|
|
debug[field] = kwargs.pop(field)
|
|
|
|
game_library = {}
|
|
for field in ('install_shortcut_desktop', 'install_shortcut_start_menu', 'disable_games_tab'):
|
|
if field in kwargs:
|
|
game_library[field] = kwargs.pop(field)
|
|
|
|
status = {}
|
|
if 'status' in kwargs:
|
|
status['status'] = _ocast(kwargs.pop('status'), str)
|
|
if 'custom_activity' in kwargs:
|
|
status['custom_status'] = (
|
|
kwargs.pop('custom_activity').to_settings_dict()
|
|
if kwargs['custom_activity'] not in {MISSING, None}
|
|
else MISSING
|
|
)
|
|
for field in ('show_current_game',):
|
|
if field in kwargs:
|
|
status[field] = kwargs.pop(field)
|
|
|
|
localization = {}
|
|
if 'locale' in kwargs:
|
|
localization['locale'] = _ocast(kwargs.pop('locale'), str)
|
|
for field in ('timezone_offset',):
|
|
if field in kwargs:
|
|
localization[field] = kwargs.pop(field)
|
|
|
|
appearance = {}
|
|
if 'theme' in kwargs:
|
|
appearance['theme'] = _ocast(kwargs.pop('theme'), int)
|
|
if 'client_theme' in kwargs:
|
|
provided: tuple = kwargs.pop('client_theme')
|
|
client_theme_settings = {} if provided is not MISSING else MISSING
|
|
if provided:
|
|
if provided[0] is not MISSING:
|
|
client_theme_settings['primary_color'] = provided[0]
|
|
if len(provided) > 1 and provided[1] is not MISSING:
|
|
client_theme_settings['background_gradient_preset_id'] = provided[1]
|
|
if len(provided) > 2 and provided[2] is not MISSING:
|
|
client_theme_settings['background_gradient_angle'] = float(provided[2])
|
|
appearance['client_theme_settings'] = client_theme_settings
|
|
if 'disable_mobile_redesign' in kwargs:
|
|
appearance['mobile_redesign_disabled'] = kwargs.pop('disable_mobile_redesign')
|
|
for field in ('developer_mode',):
|
|
if field in kwargs:
|
|
appearance[field] = kwargs.pop(field)
|
|
|
|
guild_folders = {}
|
|
if 'guild_folders' in kwargs:
|
|
guild_folders['folders'] = (
|
|
[f.to_dict() for f in kwargs.pop('guild_folders')] if kwargs['guild_folders'] is not MISSING else MISSING
|
|
)
|
|
if 'guild_positions' in kwargs:
|
|
guild_folders['guild_positions'] = (
|
|
[g.id for g in kwargs.pop('guild_positions')] if kwargs['guild_positions'] is not MISSING else MISSING
|
|
)
|
|
|
|
audio_context_settings = {}
|
|
if 'user_audio_settings' in kwargs:
|
|
audio_context_settings['user'] = (
|
|
{s.id: s.to_dict() for s in kwargs.pop('user_audio_settings')}
|
|
if kwargs['user_audio_settings'] is not MISSING
|
|
else MISSING
|
|
)
|
|
if 'stream_audio_settings' in kwargs:
|
|
audio_context_settings['stream'] = (
|
|
{s.id: s.to_dict() for s in kwargs.pop('stream_audio_settings')}
|
|
if kwargs['stream_audio_settings'] is not MISSING
|
|
else MISSING
|
|
)
|
|
|
|
communities = {}
|
|
if 'home_auto_navigation' in kwargs:
|
|
communities['disable_home_auto_nav'] = (
|
|
not kwargs.pop('home_auto_navigation') if kwargs['home_auto_navigation'] is not MISSING else MISSING
|
|
)
|
|
|
|
# Now, we do the actual patching
|
|
existing = self.to_dict()
|
|
payload = {}
|
|
for subsetting in (
|
|
'versions',
|
|
'inbox',
|
|
'guilds',
|
|
'user_content',
|
|
'voice_and_video',
|
|
'text_and_images',
|
|
'notifications',
|
|
'privacy',
|
|
'debug',
|
|
'game_library',
|
|
'status',
|
|
'localization',
|
|
'appearance',
|
|
'guild_folders',
|
|
'audio_context_settings',
|
|
'communities',
|
|
):
|
|
subsetting_dict = locals()[subsetting]
|
|
if subsetting_dict:
|
|
original = existing.get(subsetting, {})
|
|
original.update(subsetting_dict)
|
|
for k, v in dict(original).items():
|
|
if v is MISSING:
|
|
del original[k]
|
|
payload[subsetting] = original
|
|
|
|
state = self._state
|
|
require_version = self.data_version if require_version == True else require_version
|
|
ret = await state.http.edit_proto_settings(1, self.dict_to_base64(payload), require_version or None)
|
|
return UserSettings(state, ret['settings'])
|
|
|
|
|
|
class GuildFolder:
|
|
"""Represents a guild folder.
|
|
|
|
All properties have setters to faciliate editing the class for use with :meth:`UserSettings.edit`.
|
|
|
|
.. note::
|
|
|
|
Guilds not in folders *are* actually in folders API wise, with them being the only member.
|
|
|
|
These folders do not have an ID or name.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: str(x)
|
|
|
|
Returns the folder's name.
|
|
|
|
.. describe:: len(x)
|
|
|
|
Returns the number of guilds in the folder.
|
|
|
|
.. versionadded:: 1.9
|
|
|
|
.. versionchanged:: 2.0
|
|
|
|
Removed various operations and made ``id`` and ``name`` optional.
|
|
|
|
Attributes
|
|
----------
|
|
id: Optional[:class:`int`]
|
|
The ID of the folder. This is ``None`` for fake folders, as outlined in the note above.
|
|
name: Optional[:class:`str`]
|
|
The name of the folder.
|
|
"""
|
|
|
|
__slots__ = ('_state', 'id', 'name', '_colour', '_guild_ids')
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
id: Optional[int] = None,
|
|
name: Optional[str] = None,
|
|
colour: Optional[Colour] = None,
|
|
color: Optional[Colour] = None,
|
|
guilds: Sequence[Snowflake] = MISSING,
|
|
):
|
|
self._state: Optional[ConnectionState] = None
|
|
self.id: Optional[int] = id or None
|
|
self.name: Optional[str] = name or None
|
|
self._colour: Optional[int] = colour.value if colour else color.value if color else None
|
|
self._guild_ids: List[int] = [guild.id for guild in guilds] if guilds else []
|
|
|
|
def __str__(self) -> str:
|
|
return self.name or ', '.join(guild.name for guild in [guild for guild in self.guilds if isinstance(guild, Guild)])
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<GuildFolder id={self.id} name={self.name!r} guilds={self.guilds!r}>'
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._guild_ids)
|
|
|
|
@classmethod
|
|
def _from_legacy_settings(cls, *, data: Dict[str, Any], state: ConnectionState) -> Self:
|
|
self = cls.__new__(cls)
|
|
self._state = state
|
|
self.id = _get_as_snowflake(data, 'id') or None
|
|
self.name = data.get('name') or None
|
|
self._colour = data.get('color')
|
|
self._guild_ids = [int(guild_id) for guild_id in data['guild_ids']]
|
|
return self
|
|
|
|
@classmethod
|
|
def _from_settings(cls, *, data: Any, state: ConnectionState) -> Self:
|
|
"""
|
|
message GuildFolder {
|
|
repeated fixed64 guild_ids = 1;
|
|
optional google.protobuf.Int64Value id = 2;
|
|
optional google.protobuf.StringValue name = 3;
|
|
optional google.protobuf.UInt64Value color = 4;
|
|
}
|
|
"""
|
|
self = cls.__new__(cls)
|
|
self._state = state
|
|
self.id = data.id.value or None
|
|
self.name = data.name.value
|
|
self._colour = data.color.value if data.HasField('color') else None
|
|
self._guild_ids = data.guild_ids
|
|
return self
|
|
|
|
def _get_guild(self, id, /) -> Union[Guild, Object]:
|
|
from .guild import Guild # Circular import
|
|
|
|
id = int(id)
|
|
return self._state._get_or_create_unavailable_guild(id) if self._state else Object(id=id, type=Guild)
|
|
|
|
def to_dict(self) -> dict:
|
|
ret = {}
|
|
if self.id is not None:
|
|
ret['id'] = self.id
|
|
if self.name is not None:
|
|
ret['name'] = self.name
|
|
if self._colour is not None:
|
|
ret['color'] = self._colour
|
|
ret['guild_ids'] = [str(guild_id) for guild_id in self._guild_ids]
|
|
return ret
|
|
|
|
def copy(self) -> Self:
|
|
"""Returns a shallow copy of the folder."""
|
|
return self.__class__._from_legacy_settings(data=self.to_dict(), state=self._state) # type: ignore
|
|
|
|
def add_guild(self, guild: Snowflake) -> Self:
|
|
"""Adds a guild to the folder.
|
|
|
|
This function returns the class instance to allow for fluent-style
|
|
chaining.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
-----------
|
|
guild: :class:`abc.Snowflake`
|
|
The guild to add to the folder.
|
|
"""
|
|
self._guild_ids.append(guild.id)
|
|
return self
|
|
|
|
def insert_guild_at(self, index: int, guild: Snowflake) -> Self:
|
|
"""Inserts a guild before a specified index to the folder.
|
|
|
|
This function returns the class instance to allow for fluent-style
|
|
chaining.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
-----------
|
|
index: :class:`int`
|
|
The index of where to insert the field.
|
|
guild: :class:`abc.Snowflake`
|
|
The guild to add to the folder.
|
|
"""
|
|
self._guild_ids.insert(index, guild.id)
|
|
return self
|
|
|
|
def clear_guilds(self) -> None:
|
|
"""Removes all guilds from this folder.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
self._guild_ids.clear()
|
|
|
|
def remove_guild(self, index: int) -> None:
|
|
"""Removes a guild at a specified index.
|
|
|
|
If the index is invalid or out of bounds then the error is
|
|
silently swallowed.
|
|
|
|
.. note::
|
|
|
|
When deleting a field by index, the index of the other fields
|
|
shift to fill the gap just like a regular list.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
-----------
|
|
index: :class:`int`
|
|
The index of the field to remove.
|
|
"""
|
|
try:
|
|
del self._guild_ids[index]
|
|
except IndexError:
|
|
pass
|
|
|
|
def set_guild_at(self, index: int, guild: Snowflake) -> Self:
|
|
"""Modifies a guild to the guild object.
|
|
|
|
The index must point to a valid pre-existing guild.
|
|
|
|
This function returns the class instance to allow for fluent-style
|
|
chaining.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Parameters
|
|
-----------
|
|
index: :class:`int`
|
|
The index of the field to modify.
|
|
guild: :class:`abc.Snowflake`
|
|
The guild to add to the folder.
|
|
|
|
Raises
|
|
-------
|
|
IndexError
|
|
An invalid index was provided.
|
|
"""
|
|
self._guild_ids[index] = guild.id
|
|
|
|
try:
|
|
self._guild_ids[index] = guild.id
|
|
except (TypeError, IndexError):
|
|
raise IndexError('field index out of range')
|
|
return self
|
|
|
|
@property
|
|
def guilds(self) -> List[Union[Guild, Object]]:
|
|
"""List[Union[:class:`Guild`, :class:`Object`]]: The guilds in the folder. Always :class:`Object` if state is not attached."""
|
|
return [self._get_guild(guild_id) for guild_id in self._guild_ids]
|
|
|
|
@guilds.setter
|
|
def guilds(self, value: Sequence[Snowflake]) -> None:
|
|
self._guild_ids = [guild.id for guild in value]
|
|
|
|
@property
|
|
def colour(self) -> Optional[Colour]:
|
|
"""Optional[:class:`Colour`]: The colour code of the folder. There is an alias for this named :attr:`colour`."""
|
|
return Colour(self._colour) if self._colour is not None else None
|
|
|
|
@colour.setter
|
|
def colour(self, value: Optional[Union[int, Colour]]) -> None:
|
|
if value is None:
|
|
self._colour = None
|
|
elif isinstance(value, Colour):
|
|
self._colour = value.value
|
|
elif isinstance(value, int):
|
|
self._colour = value
|
|
else:
|
|
raise TypeError(f'Expected discord.Colour, int, or None but received {value.__class__.__name__} instead.')
|
|
|
|
@property
|
|
def color(self) -> Optional[Colour]:
|
|
"""Optional[:class:`Colour`]: The colour code of the folder. There is an alias for this named :attr:`colour`."""
|
|
return self.colour
|
|
|
|
@color.setter
|
|
def color(self, value: Optional[Union[int, Colour]]) -> None:
|
|
self.colour = value
|
|
|
|
|
|
class GuildProgress:
|
|
"""Represents a guild's settings revolving around upsells, promotions, and feature progress.
|
|
|
|
All properties have setters to faciliate editing the class for use with :meth:`UserSettings.edit`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
guild_id: :class:`int`
|
|
The ID of the guild.
|
|
recents_dismissed_at: Optional[:class:`datetime.datetime`]
|
|
When the guild recents were last dismissed.
|
|
"""
|
|
|
|
__slots__ = (
|
|
'guild_id',
|
|
'_hub_progress',
|
|
'_onboarding_progress',
|
|
'recents_dismissed_at',
|
|
'_dismissed_contents',
|
|
'_collapsed_channel_ids',
|
|
'_state',
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
guild_id: int,
|
|
*,
|
|
hub_progress: HubProgressFlags,
|
|
onboarding_progress: OnboardingProgressFlags,
|
|
recents_dismissed_at: Optional[datetime] = None,
|
|
dismissed_contents: Sequence[int] = MISSING,
|
|
collapsed_channels: List[Snowflake] = MISSING,
|
|
) -> None:
|
|
self._state: Optional[ConnectionState] = None
|
|
self.guild_id = guild_id
|
|
self._hub_progress = hub_progress.value
|
|
self._onboarding_progress = onboarding_progress.value
|
|
self.recents_dismissed_at: Optional[datetime] = recents_dismissed_at
|
|
self._dismissed_contents = self._pack_dismissed_contents(dismissed_contents or [])
|
|
self._collapsed_channel_ids = [channel.id for channel in collapsed_channels] or []
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<GuildProgress guild_id={self.guild_id} hub_progress={self.hub_progress!r} onboarding_progress={self.onboarding_progress!r}>'
|
|
|
|
@classmethod
|
|
def _from_settings(cls, guild_id: int, *, data: Any, state: ConnectionState) -> Self:
|
|
"""
|
|
message ChannelSettings {
|
|
bool collapsed_in_inbox = 1;
|
|
}
|
|
|
|
message GuildSettings {
|
|
map<fixed64, ChannelSettings> channels = 1;
|
|
uint32 hub_progress = 2;
|
|
uint32 guild_onboarding_progress = 3;
|
|
optional google.protobuf.Timestamp guild_recents_dismissed_at = 4;
|
|
bytes dismissed_guild_content = 5;
|
|
}
|
|
|
|
message AllGuildSettings {
|
|
map<fixed64, GuildSettings> guilds = 1;
|
|
}
|
|
"""
|
|
self = cls.__new__(cls)
|
|
self._state = state
|
|
self.guild_id = guild_id
|
|
self._hub_progress = data.hub_progress
|
|
self._onboarding_progress = data.guild_onboarding_progress
|
|
self.recents_dismissed_at = (
|
|
data.guild_recents_dismissed_at.ToDatetime(tzinfo=timezone.utc)
|
|
if data.HasField('guild_recents_dismissed_at')
|
|
else None
|
|
)
|
|
self._dismissed_contents = data.dismissed_guild_content
|
|
self._collapsed_channel_ids = [
|
|
channel_id for channel_id, settings in data.channels.items() if settings.collapsed_in_inbox
|
|
]
|
|
return self
|
|
|
|
def _get_channel(self, id: int, /) -> Union[GuildChannel, Object]:
|
|
id = int(id)
|
|
return self.guild.get_channel(id) or Object(id=id) if self.guild is not None else Object(id=id)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
data = {
|
|
'hub_progress': self._hub_progress,
|
|
'guild_onboarding_progress': self._onboarding_progress,
|
|
'dismissed_guild_content': self._dismissed_contents,
|
|
'channels': {id: {'collapsed_in_inbox': True} for id in self._collapsed_channel_ids},
|
|
}
|
|
if self.recents_dismissed_at is not None:
|
|
data['guild_recents_dismissed_at'] = self.recents_dismissed_at.isoformat()
|
|
return data
|
|
|
|
def copy(self) -> Self:
|
|
"""Returns a shallow copy of the progress settings."""
|
|
cls = self.__class__(self.guild_id, hub_progress=self.hub_progress, onboarding_progress=self.onboarding_progress, recents_dismissed_at=self.recents_dismissed_at, dismissed_contents=self.dismissed_contents, collapsed_channels=self.collapsed_channels) # type: ignore
|
|
cls._state = self._state
|
|
return cls
|
|
|
|
@property
|
|
def guild(self) -> Optional[Guild]:
|
|
"""Optional[:class:`Guild`]: The guild this progress belongs to. ``None`` if state is not attached."""
|
|
return self._state._get_or_create_unavailable_guild(self.guild_id) if self._state is not None else None
|
|
|
|
@property
|
|
def hub_progress(self) -> HubProgressFlags:
|
|
""":class:`HubProgressFlags`: The hub's usage and feature progress."""
|
|
return HubProgressFlags._from_value(self._hub_progress)
|
|
|
|
@hub_progress.setter
|
|
def hub_progress(self, value: HubProgressFlags) -> None:
|
|
self._hub_progress = value.value
|
|
|
|
@property
|
|
def onboarding_progress(self) -> OnboardingProgressFlags:
|
|
""":class:`OnboardingProgressFlags`: The guild's onboarding usage and feature progress."""
|
|
return OnboardingProgressFlags._from_value(self._onboarding_progress)
|
|
|
|
@onboarding_progress.setter
|
|
def onboarding_progress(self, value: OnboardingProgressFlags) -> None:
|
|
self._onboarding_progress = value.value
|
|
|
|
@staticmethod
|
|
def _pack_dismissed_contents(contents: Sequence[int]) -> bytes:
|
|
return struct.pack(f'>{len(contents)}B', *contents)
|
|
|
|
@property
|
|
def dismissed_contents(self) -> Tuple[int, ...]:
|
|
"""Tuple[:class:`int`]: A list of enum values representing per-guild dismissable content in the app.
|
|
|
|
.. note::
|
|
|
|
For now, this just returns the raw values without converting to a proper enum,
|
|
as the enum values change too often to be viably maintained.
|
|
"""
|
|
contents = self._dismissed_contents
|
|
return struct.unpack(f'>{len(contents)}B', contents)
|
|
|
|
@dismissed_contents.setter
|
|
def dismissed_contents(self, value: Sequence[int]) -> None:
|
|
self._dismissed_contents = self._pack_dismissed_contents(value)
|
|
|
|
@property
|
|
def collapsed_channels(self) -> List[Union[GuildChannel, Object]]:
|
|
"""List[Union[:class:`abc.GuildChannel`, :class:`Object`]]: A list of guild channels that are collapsed in the inbox. Always :class:`Object` if state is not attached."""
|
|
return list(map(self._get_channel, self._collapsed_channel_ids))
|
|
|
|
@collapsed_channels.setter
|
|
def collapsed_channels(self, value: Sequence[Snowflake]) -> None:
|
|
self._collapsed_channel_ids = [channel.id for channel in value]
|
|
|
|
|
|
class AudioContext:
|
|
"""Represents saved audio settings for a user or stream.
|
|
|
|
All properties have setters to faciliate editing the class for use with :meth:`UserSettings.edit`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
user_id: :class:`int`
|
|
The ID of the user.
|
|
muted: :class:`bool`
|
|
Whether the user or stream is muted.
|
|
volume: :class:`float`
|
|
The volume of the user or stream (0-100).
|
|
modified_at: :class:`datetime.datetime`
|
|
The time the settings were last modified.
|
|
"""
|
|
|
|
__slots__ = ('_state', 'user_id', 'muted', 'volume', 'modified_at')
|
|
|
|
def __init__(self, user_id: int, *, muted: bool = False, volume: float) -> None:
|
|
self._state: Optional[ConnectionState] = None
|
|
self.user_id = user_id
|
|
self.muted = muted
|
|
self.volume = volume
|
|
self.modified_at = utcnow()
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f'<AudioContext user_id={self.user_id} muted={self.muted} volume={self.volume} modified_at={self.modified_at!r}>'
|
|
)
|
|
|
|
@classmethod
|
|
def _from_settings(cls, user_id: int, *, data: Any, state: ConnectionState) -> Self:
|
|
"""
|
|
message AudioContextSetting {
|
|
bool muted = 1;
|
|
float volume = 2;
|
|
fixed64 modified_at = 3;
|
|
}
|
|
"""
|
|
self = cls.__new__(cls)
|
|
self._state = state
|
|
self.user_id = user_id
|
|
self.muted = data.muted
|
|
self.volume = data.volume
|
|
self.modified_at = parse_timestamp(data.modified_at)
|
|
return self
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Converts the object to a dictionary."""
|
|
return {
|
|
'user_id': self.user_id,
|
|
'muted': self.muted,
|
|
'volume': self.volume,
|
|
'modified_at': self.modified_at.isoformat(),
|
|
}
|
|
|
|
def copy(self) -> Self:
|
|
"""Returns a shallow copy of the audio context."""
|
|
cls = self.__class__(self.user_id, muted=self.muted, volume=self.volume)
|
|
cls.modified_at = self.modified_at
|
|
cls._state = self._state
|
|
return cls
|
|
|
|
@property
|
|
def user(self) -> Optional[User]:
|
|
"""Optional[:class:`User`]: The user the settings are for. ``None`` if state is not attached."""
|
|
return self._state.get_user(self.user_id) if self._state is not None else None
|
|
|
|
|
|
class LegacyUserSettings:
|
|
"""Represents the legacy Discord client settings.
|
|
|
|
.. versionadded:: 1.9
|
|
|
|
.. deprecated:: 2.0
|
|
|
|
.. note::
|
|
|
|
Discord has migrated user settings to a new protocol buffer format.
|
|
While these legacy settings still exist, they are no longer sent to newer clients (so they will have to be fetched).
|
|
|
|
The new settings are available in :class:`UserSettings`, and this class has been deprecated and renamed to :class:`LegacyUserSettings`.
|
|
All options in this class are available in the new format, and changes are reflected in both.
|
|
|
|
Attributes
|
|
----------
|
|
afk_timeout: :class:`int`
|
|
How long (in seconds) the user needs to be AFK until Discord
|
|
sends push notifications to mobile devices (30-600).
|
|
allow_accessibility_detection: :class:`bool`
|
|
Whether to allow Discord to track screen reader usage.
|
|
animate_emojis: :class:`bool`
|
|
Whether to animate emojis in the chat.
|
|
contact_sync_enabled: :class:`bool`
|
|
Whether to enable the contact sync on Discord mobile.
|
|
convert_emoticons: :class:`bool`
|
|
Whether to automatically convert emoticons into emojis (e.g. :) -> 😃).
|
|
default_guilds_restricted: :class:`bool`
|
|
Whether to automatically disable DMs between you and
|
|
members of new guilds you join.
|
|
detect_platform_accounts: :class:`bool`
|
|
Whether to automatically detect accounts from services
|
|
like Steam and Blizzard when you open the Discord client.
|
|
developer_mode: :class:`bool`
|
|
Whether to enable developer mode.
|
|
disable_games_tab: :class:`bool`
|
|
Whether to disable the showing of the Games tab.
|
|
enable_tts_command: :class:`bool`
|
|
Whether to allow TTS messages to be played/sent.
|
|
gif_auto_play: :class:`bool`
|
|
Whether to automatically play GIFs that are in the chat.
|
|
inline_attachment_media: :class:`bool`
|
|
Whether to display attachments when they are uploaded in chat.
|
|
inline_embed_media: :class:`bool`
|
|
Whether to display videos and images from links posted in chat.
|
|
message_display_compact: :class:`bool`
|
|
Whether to use the compact Discord display mode.
|
|
native_phone_integration_enabled: :class:`bool`
|
|
Whether to enable the new Discord mobile phone number friend
|
|
requesting features.
|
|
render_embeds: :class:`bool`
|
|
Whether to render embeds that are sent in the chat.
|
|
render_reactions: :class:`bool`
|
|
Whether to render reactions that are added to messages.
|
|
show_current_game: :class:`bool`
|
|
Whether to display the game that you are currently playing.
|
|
stream_notifications_enabled: :class:`bool`
|
|
Whether stream notifications for friends will be received.
|
|
timezone_offset: :class:`int`
|
|
The timezone offset from UTC to use (in minutes).
|
|
view_nsfw_commands: :class:`bool`
|
|
Whether to show NSFW application commands in DMs.
|
|
|
|
.. versionadded:: 2.0
|
|
view_nsfw_guilds: :class:`bool`
|
|
Whether to show NSFW guilds on iOS.
|
|
"""
|
|
|
|
if TYPE_CHECKING: # Fuck me
|
|
afk_timeout: int
|
|
allow_accessibility_detection: bool
|
|
animate_emojis: bool
|
|
contact_sync_enabled: bool
|
|
convert_emoticons: bool
|
|
default_guilds_restricted: bool
|
|
detect_platform_accounts: bool
|
|
developer_mode: bool
|
|
disable_games_tab: bool
|
|
enable_tts_command: bool
|
|
gif_auto_play: bool
|
|
inline_attachment_media: bool
|
|
inline_embed_media: bool
|
|
message_display_compact: bool
|
|
native_phone_integration_enabled: bool
|
|
render_embeds: bool
|
|
render_reactions: bool
|
|
show_current_game: bool
|
|
stream_notifications_enabled: bool
|
|
timezone_offset: int
|
|
view_nsfw_commands: bool
|
|
view_nsfw_guilds: bool
|
|
|
|
def __init__(self, *, data, state: ConnectionState) -> None:
|
|
self._state = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return '<LegacyUserSettings>'
|
|
|
|
def _get_guild(self, id: int, /) -> Guild:
|
|
return self._state._get_or_create_unavailable_guild(int(id))
|
|
|
|
def _update(self, data: Dict[str, Any]) -> None:
|
|
RAW_VALUES = {
|
|
'afk_timeout',
|
|
'allow_accessibility_detection',
|
|
'animate_emojis',
|
|
'contact_sync_enabled',
|
|
'convert_emoticons',
|
|
'default_guilds_restricted',
|
|
'detect_platform_accounts',
|
|
'developer_mode',
|
|
'disable_games_tab',
|
|
'enable_tts_command',
|
|
'gif_auto_play',
|
|
'inline_attachment_media',
|
|
'inline_embed_media',
|
|
'message_display_compact',
|
|
'native_phone_integration_enabled',
|
|
'render_embeds',
|
|
'render_reactions',
|
|
'show_current_game',
|
|
'stream_notifications_enabled',
|
|
'timezone_offset',
|
|
'view_nsfw_commands',
|
|
'view_nsfw_guilds',
|
|
}
|
|
|
|
for key, value in data.items():
|
|
if key in RAW_VALUES:
|
|
setattr(self, key, value)
|
|
else:
|
|
setattr(self, '_' + key, value)
|
|
|
|
async def edit(self, **kwargs) -> Self:
|
|
"""|coro|
|
|
|
|
Edits the client user's settings.
|
|
|
|
.. versionchanged:: 2.0
|
|
The edit is no longer in-place, instead the newly edited settings are returned.
|
|
|
|
.. deprecated:: 2.0
|
|
|
|
Parameters
|
|
----------
|
|
activity_restricted_guilds: List[:class:`~discord.abc.Snowflake`]
|
|
A list of guilds that your current activity will not be shown in.
|
|
|
|
.. versionadded:: 2.0
|
|
activity_joining_restricted_guilds: List[:class:`~discord.abc.Snowflake`]
|
|
A list of guilds that will not be able to join your current activity.
|
|
|
|
.. versionadded:: 2.0
|
|
afk_timeout: :class:`int`
|
|
How long (in seconds) the user needs to be AFK until Discord
|
|
sends push notifications to mobile device (30-600).
|
|
allow_accessibility_detection: :class:`bool`
|
|
Whether to allow Discord to track screen reader usage.
|
|
animate_emojis: :class:`bool`
|
|
Whether to animate emojis in the chat.
|
|
animate_stickers: :class:`.StickerAnimationOptions`
|
|
Whether to animate stickers in the chat.
|
|
contact_sync_enabled: :class:`bool`
|
|
Whether to enable the contact sync on Discord mobile.
|
|
convert_emoticons: :class:`bool`
|
|
Whether to automatically convert emoticons into emojis (e.g. :) -> 😃).
|
|
default_guilds_restricted: :class:`bool`
|
|
Whether to automatically disable DMs between you and
|
|
members of new guilds you join.
|
|
detect_platform_accounts: :class:`bool`
|
|
Whether to automatically detect accounts from services
|
|
like Steam and Blizzard when you open the Discord client.
|
|
developer_mode: :class:`bool`
|
|
Whether to enable developer mode.
|
|
disable_games_tab: :class:`bool`
|
|
Whether to disable the showing of the Games tab.
|
|
enable_tts_command: :class:`bool`
|
|
Whether 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:`.FriendSourceFlags`
|
|
Who can add you as a friend.
|
|
friend_discovery_flags: :class:`.FriendDiscoveryFlags`
|
|
How you get recommended friends.
|
|
gif_auto_play: :class:`bool`
|
|
Whether to automatically play GIFs that are in the chat.
|
|
guild_positions: List[:class:`~discord.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 to display attachments when they are uploaded in chat.
|
|
inline_embed_media: :class:`bool`
|
|
Whether to display videos and images from links posted in chat.
|
|
locale: :class:`.Locale`
|
|
The :rfc:`3066` language identifier of the locale to use for the language
|
|
of the Discord client.
|
|
message_display_compact: :class:`bool`
|
|
Whether to use the compact Discord display mode.
|
|
native_phone_integration_enabled: :class:`bool`
|
|
Whether to enable the new Discord mobile phone number friend
|
|
requesting features.
|
|
passwordless: :class:`bool`
|
|
Whether to enable passwordless login.
|
|
render_embeds: :class:`bool`
|
|
Whether to render embeds that are sent in the chat.
|
|
render_reactions: :class:`bool`
|
|
Whether to render reactions that are added to messages.
|
|
restricted_guilds: List[:class:`~discord.abc.Snowflake`]
|
|
A list of guilds that you will not receive DMs from.
|
|
show_current_game: :class:`bool`
|
|
Whether to display the game that you are currently playing.
|
|
stream_notifications_enabled: :class:`bool`
|
|
Whether stream notifications for friends will be received.
|
|
theme: :class:`.Theme`
|
|
The overall theme of the Discord UI.
|
|
timezone_offset: :class:`int`
|
|
The timezone offset to use.
|
|
view_nsfw_commands: :class:`bool`
|
|
Whether to show NSFW application commands in DMs.
|
|
|
|
.. versionadded:: 2.0
|
|
view_nsfw_guilds: :class:`bool`
|
|
Whether to show NSFW guilds on iOS.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Editing the settings failed.
|
|
|
|
Returns
|
|
-------
|
|
:class:`.UserSettings`
|
|
The client user's updated settings.
|
|
"""
|
|
return await self._state.client.edit_legacy_settings(**kwargs)
|
|
|
|
@property
|
|
def activity_restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that your current activity will not be shown in.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
return list(map(self._get_guild, getattr(self, '_activity_restricted_guild_ids', [])))
|
|
|
|
@property
|
|
def activity_joining_restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that will not be able to join your current activity.
|
|
|
|
.. versionadded:: 2.0
|
|
"""
|
|
return list(map(self._get_guild, getattr(self, '_activity_joining_restricted_guild_ids', [])))
|
|
|
|
@property
|
|
def animate_stickers(self) -> StickerAnimationOptions:
|
|
""":class:`StickerAnimationOptions`: Whether to animate stickers in the chat."""
|
|
return try_enum(StickerAnimationOptions, getattr(self, '_animate_stickers', 0))
|
|
|
|
@property
|
|
def custom_activity(self) -> Optional[CustomActivity]:
|
|
"""Optional[:class:`CustomActivity`]: The set custom activity."""
|
|
return CustomActivity._from_legacy_settings(data=getattr(self, '_custom_status', None), state=self._state)
|
|
|
|
@property
|
|
def explicit_content_filter(self) -> UserContentFilter:
|
|
""":class:`UserContentFilter`: The filter for explicit content in all messages."""
|
|
return try_enum(UserContentFilter, getattr(self, '_explicit_content_filter', 0))
|
|
|
|
@property
|
|
def friend_source_flags(self) -> FriendSourceFlags:
|
|
""":class:`FriendSourceFlags`: Who can add you as a friend."""
|
|
return FriendSourceFlags._from_dict(getattr(self, '_friend_source_flags', {'all': True}))
|
|
|
|
@property
|
|
def friend_discovery_flags(self) -> FriendDiscoveryFlags:
|
|
""":class:`FriendDiscoveryFlags`: How you get recommended friends."""
|
|
return FriendDiscoveryFlags._from_value(getattr(self, '_friend_discovery_flags', 0))
|
|
|
|
@property
|
|
def guild_folders(self) -> List[GuildFolder]:
|
|
"""List[:class:`GuildFolder`]: A list of guild folders."""
|
|
state = self._state
|
|
return [
|
|
GuildFolder._from_legacy_settings(data=folder, state=state) for folder in getattr(self, '_guild_folders', [])
|
|
]
|
|
|
|
@property
|
|
def guild_positions(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds in order of the guild/guild icons that are on the left hand side of the UI."""
|
|
return list(map(self._get_guild, getattr(self, '_guild_positions', [])))
|
|
|
|
@property
|
|
def locale(self) -> Locale:
|
|
""":class:`Locale`: The :rfc:`3066` language identifier
|
|
of the locale to use for the language of the Discord client.
|
|
|
|
.. versionchanged:: 2.0
|
|
This now returns a :class:`Locale` object instead of a string.
|
|
"""
|
|
return try_enum(Locale, getattr(self, '_locale', 'en-US'))
|
|
|
|
@property
|
|
def passwordless(self) -> bool:
|
|
""":class:`bool`: Whether to enable passwordless login."""
|
|
return getattr(self, '_passwordless', False)
|
|
|
|
@property
|
|
def restricted_guilds(self) -> List[Guild]:
|
|
"""List[:class:`Guild`]: A list of guilds that you will not receive DMs from."""
|
|
return list(map(self._get_guild, getattr(self, '_restricted_guilds', [])))
|
|
|
|
@property
|
|
def status(self) -> Status:
|
|
"""Optional[:class:`Status`]: The configured status."""
|
|
return try_enum(Status, getattr(self, '_status', 'online'))
|
|
|
|
@property
|
|
def theme(self) -> Theme:
|
|
""":class:`Theme`: The overall theme of the Discord UI."""
|
|
return try_enum(Theme, getattr(self, '_theme', 'dark')) # Sane default :)
|
|
|
|
|
|
class MuteConfig:
|
|
"""An object representing an object's mute status.
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: x == y
|
|
|
|
Checks if two items are muted.
|
|
|
|
.. describe:: x != y
|
|
|
|
Checks if two items are not muted.
|
|
|
|
.. describe:: str(x)
|
|
|
|
Returns the mute status as a string.
|
|
|
|
.. describe:: int(x)
|
|
|
|
Returns the mute status as an int.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
muted: :class:`bool`
|
|
Indicates if the object is muted.
|
|
until: Optional[:class:`datetime.datetime`]
|
|
When the mute will expire.
|
|
"""
|
|
|
|
def __init__(self, muted: bool, config: Optional[MuteConfigPayload] = None) -> None:
|
|
until = parse_time(config.get('end_time') if config else None)
|
|
if until is not None:
|
|
if until <= utcnow():
|
|
muted = False
|
|
until = None
|
|
|
|
self.muted: bool = muted
|
|
self.until: Optional[datetime] = until
|
|
|
|
def __repr__(self) -> str:
|
|
return str(self.muted)
|
|
|
|
def __int__(self) -> int:
|
|
return int(self.muted)
|
|
|
|
def __bool__(self) -> bool:
|
|
return self.muted
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
return self.muted == bool(other)
|
|
|
|
def __ne__(self, other: object) -> bool:
|
|
return not self.muted == bool(other)
|
|
|
|
|
|
class ChannelSettings:
|
|
"""Represents a channel's notification settings.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
level: :class:`NotificationLevel`
|
|
The notification level for the channel.
|
|
muted: :class:`MuteConfig`
|
|
The mute configuration for the channel.
|
|
collapsed: :class:`bool`
|
|
Whether the channel is collapsed.
|
|
Only applicable to channels of type :attr:`ChannelType.category`.
|
|
"""
|
|
|
|
if TYPE_CHECKING:
|
|
_channel_id: int
|
|
level: NotificationLevel
|
|
muted: MuteConfig
|
|
collapsed: bool
|
|
|
|
def __init__(self, guild_id: Optional[int] = None, *, data: ChannelOverridePayload, state: ConnectionState) -> None:
|
|
self._guild_id = guild_id
|
|
self._state = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<ChannelSettings channel={self.channel} level={self.level} muted={self.muted} collapsed={self.collapsed}>'
|
|
|
|
def _update(self, data: ChannelOverridePayload) -> None:
|
|
# We consider everything optional because this class can be constructed with no data
|
|
# to represent the default settings
|
|
self._channel_id = int(data['channel_id'])
|
|
self.collapsed = data.get('collapsed', False)
|
|
|
|
self.level = try_enum(NotificationLevel, data.get('message_notifications', 3))
|
|
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config'))
|
|
|
|
@property
|
|
def channel(self) -> Union[GuildChannel, PrivateChannel]:
|
|
"""Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`]: Returns the channel these settings are for."""
|
|
guild = self._state._get_or_create_unavailable_guild(self._guild_id) if self._guild_id else None
|
|
if guild:
|
|
channel = guild.get_channel(self._channel_id)
|
|
else:
|
|
channel = self._state._get_private_channel(self._channel_id)
|
|
if not channel:
|
|
channel = Object(id=self._channel_id)
|
|
return channel # type: ignore # Lying for better developer UX
|
|
|
|
async def edit(
|
|
self,
|
|
*,
|
|
muted_until: Optional[Union[bool, datetime]] = MISSING,
|
|
collapsed: bool = MISSING,
|
|
level: NotificationLevel = MISSING,
|
|
) -> ChannelSettings:
|
|
"""|coro|
|
|
|
|
Edits the channel's notification settings.
|
|
|
|
All parameters are optional.
|
|
|
|
Parameters
|
|
-----------
|
|
muted_until: Optional[Union[:class:`datetime.datetime`, :class:`bool`]]
|
|
The date this channel's mute should expire.
|
|
This can be ``True`` to mute indefinitely, or ``False``/``None`` to unmute.
|
|
|
|
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
|
|
collapsed: :class:`bool`
|
|
Indicates if the channel should be collapsed or not.
|
|
Only applicable to channels of type :attr:`ChannelType.category`.
|
|
level: :class:`NotificationLevel`
|
|
Determines what level of notifications you receive for the channel.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Editing the settings failed.
|
|
|
|
Returns
|
|
--------
|
|
:class:`ChannelSettings`
|
|
The new notification settings.
|
|
"""
|
|
state = self._state
|
|
guild_id = self._guild_id
|
|
channel_id = self._channel_id
|
|
payload = {}
|
|
|
|
if muted_until is not MISSING:
|
|
if not muted_until:
|
|
payload['muted'] = False
|
|
else:
|
|
payload['muted'] = True
|
|
if muted_until is True:
|
|
payload['mute_config'] = {'selected_time_window': -1, 'end_time': None}
|
|
else:
|
|
if muted_until.tzinfo is None:
|
|
raise TypeError(
|
|
'muted_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
|
|
)
|
|
|
|
mute_config = {
|
|
'selected_time_window': (muted_until - utcnow()).total_seconds(),
|
|
'end_time': muted_until.isoformat(),
|
|
}
|
|
payload['mute_config'] = mute_config
|
|
|
|
if collapsed is not MISSING:
|
|
payload['collapsed'] = collapsed
|
|
|
|
if level is not MISSING:
|
|
payload['message_notifications'] = level.value
|
|
|
|
fields = {'channel_overrides': {str(channel_id): payload}}
|
|
data = await state.http.edit_guild_settings(guild_id or '@me', fields)
|
|
|
|
override = find(lambda x: x.get('channel_id') == str(channel_id), data['channel_overrides']) or {
|
|
'channel_id': channel_id
|
|
}
|
|
return ChannelSettings(guild_id, data=override, state=state) # type: ignore
|
|
|
|
|
|
class GuildSettings:
|
|
"""Represents a guild's notification settings.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
level: :class:`NotificationLevel`
|
|
The notification level for the guild.
|
|
muted: :class:`MuteConfig`
|
|
The mute configuration for the guild.
|
|
suppress_everyone: :class:`bool`
|
|
Whether to suppress @everyone/@here notifications.
|
|
suppress_roles: :class:`bool`
|
|
Whether to suppress role notifications.
|
|
hide_muted_channels: :class:`bool`
|
|
Whether to hide muted channels.
|
|
mobile_push: :class:`bool`
|
|
Whether to enable mobile push notifications.
|
|
mute_scheduled_events: :class:`bool`
|
|
Whether to mute scheduled events.
|
|
notify_highlights: :class:`HighlightLevel`
|
|
Whether to include highlights in notifications.
|
|
version: :class:`int`
|
|
The version of the guild's settings.
|
|
"""
|
|
|
|
if TYPE_CHECKING:
|
|
_channel_overrides: Dict[int, ChannelSettings]
|
|
_guild_id: Optional[int]
|
|
level: NotificationLevel
|
|
muted: MuteConfig
|
|
suppress_everyone: bool
|
|
suppress_roles: bool
|
|
hide_muted_channels: bool
|
|
mobile_push: bool
|
|
mute_scheduled_events: bool
|
|
notify_highlights: HighlightLevel
|
|
version: int
|
|
|
|
def __init__(self, *, data: UserGuildSettingsPayload, state: ConnectionState) -> None:
|
|
self._state = state
|
|
self.version = -1 # Overriden by real data
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<GuildSettings guild={self.guild!r} level={self.level} muted={self.muted} suppress_everyone={self.suppress_everyone} suppress_roles={self.suppress_roles}>'
|
|
|
|
def _update(self, data: UserGuildSettingsPayload) -> None:
|
|
# We consider everything optional because this class can be constructed with no data
|
|
# to represent the default settings
|
|
self._guild_id = guild_id = _get_as_snowflake(data, 'guild_id')
|
|
self.level = try_enum(NotificationLevel, data.get('message_notifications', 3))
|
|
self.suppress_everyone = data.get('suppress_everyone', False)
|
|
self.suppress_roles = data.get('suppress_roles', False)
|
|
self.hide_muted_channels = data.get('hide_muted_channels', False)
|
|
self.mobile_push = data.get('mobile_push', True)
|
|
self.mute_scheduled_events = data.get('mute_scheduled_events', False)
|
|
self.notify_highlights = try_enum(HighlightLevel, data.get('notify_highlights', 0))
|
|
self.version = data.get('version', self.version)
|
|
|
|
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config'))
|
|
self._channel_overrides = overrides = {}
|
|
state = self._state
|
|
for override in data.get('channel_overrides', []):
|
|
channel_id = int(override['channel_id'])
|
|
overrides[channel_id] = ChannelSettings(guild_id, data=override, state=state)
|
|
|
|
@property
|
|
def guild(self) -> Union[Guild, ClientUser]:
|
|
"""Union[:class:`Guild`, :class:`ClientUser`]: Returns the guild that these settings are for.
|
|
|
|
If the returned value is a :class:`ClientUser` then the settings are for the user's private channels.
|
|
"""
|
|
if self._guild_id:
|
|
return self._state._get_or_create_unavailable_guild(self._guild_id)
|
|
return self._state.user # type: ignore # Should always be present here
|
|
|
|
@property
|
|
def channel_overrides(self) -> List[ChannelSettings]:
|
|
"""List[:class:`ChannelSettings`: Returns a list of all the overrided channel notification settings."""
|
|
return list(self._channel_overrides.values())
|
|
|
|
async def edit(
|
|
self,
|
|
muted_until: Optional[Union[bool, datetime]] = MISSING,
|
|
level: NotificationLevel = MISSING,
|
|
suppress_everyone: bool = MISSING,
|
|
suppress_roles: bool = MISSING,
|
|
mobile_push: bool = MISSING,
|
|
hide_muted_channels: bool = MISSING,
|
|
mute_scheduled_events: bool = MISSING,
|
|
notify_highlights: HighlightLevel = MISSING,
|
|
) -> Optional[GuildSettings]:
|
|
"""|coro|
|
|
|
|
Edits the guild's notification settings.
|
|
|
|
All parameters are optional.
|
|
|
|
Parameters
|
|
-----------
|
|
muted_until: Optional[Union[:class:`datetime.datetime`, :class:`bool`]]
|
|
The date this guild's mute should expire.
|
|
This can be ``True`` to mute indefinitely, or ``False``/``None`` to unmute.
|
|
|
|
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
|
|
level: :class:`NotificationLevel`
|
|
Determines what level of notifications you receive for the guild.
|
|
suppress_everyone: :class:`bool`
|
|
Indicates if @everyone mentions should be suppressed for the guild.
|
|
suppress_roles: :class:`bool`
|
|
Indicates if role mentions should be suppressed for the guild.
|
|
mobile_push: :class:`bool`
|
|
Indicates if push notifications should be sent to mobile devices for this guild.
|
|
hide_muted_channels: :class:`bool`
|
|
Indicates if channels that are muted should be hidden from the sidebar.
|
|
mute_scheduled_events: :class:`bool`
|
|
Indicates if scheduled events should be muted.
|
|
notify_highlights: :class:`HighlightLevel`
|
|
Indicates if highlights should be included in notifications.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Editing the settings failed.
|
|
|
|
Returns
|
|
--------
|
|
:class:`GuildSettings`
|
|
The new notification settings.
|
|
"""
|
|
payload = {}
|
|
|
|
if muted_until is not MISSING:
|
|
if not muted_until:
|
|
payload['muted'] = False
|
|
else:
|
|
payload['muted'] = True
|
|
if muted_until is not True:
|
|
if muted_until.tzinfo is None:
|
|
raise TypeError(
|
|
'muted_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
|
|
)
|
|
|
|
mute_config = {
|
|
'selected_time_window': (muted_until - utcnow()).total_seconds(),
|
|
'end_time': muted_until.isoformat(),
|
|
}
|
|
payload['mute_config'] = mute_config
|
|
if level is not MISSING:
|
|
payload['message_notifications'] = level.value
|
|
if suppress_everyone is not MISSING:
|
|
payload['suppress_everyone'] = suppress_everyone
|
|
if suppress_roles is not MISSING:
|
|
payload['suppress_roles'] = suppress_roles
|
|
if mobile_push is not MISSING:
|
|
payload['mobile_push'] = mobile_push
|
|
if hide_muted_channels is not MISSING:
|
|
payload['hide_muted_channels'] = hide_muted_channels
|
|
if mute_scheduled_events is not MISSING:
|
|
payload['mute_scheduled_events'] = mute_scheduled_events
|
|
if notify_highlights is not MISSING:
|
|
payload['notify_highlights'] = notify_highlights.value
|
|
|
|
data = await self._state.http.edit_guild_settings(self._guild_id or '@me', payload)
|
|
return GuildSettings(data=data, state=self._state)
|
|
|
|
|
|
class TrackingSettings:
|
|
"""Represents your Discord tracking settings.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
.. container:: operations
|
|
|
|
.. describe:: bool(x)
|
|
|
|
Checks if any tracking settings are enabled.
|
|
|
|
Attributes
|
|
----------
|
|
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.
|
|
"""
|
|
|
|
__slots__ = ('_state', 'personalization', 'usage_statistics')
|
|
|
|
def __init__(
|
|
self, *, data: Union[PartialConsentSettingsPayload, ConsentSettingsPayload], state: ConnectionState
|
|
) -> None:
|
|
self._state = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<TrackingSettings personalization={self.personalization} usage_statistics={self.usage_statistics}>'
|
|
|
|
def __bool__(self) -> bool:
|
|
return any((self.personalization, self.usage_statistics))
|
|
|
|
def _update(self, data: Union[PartialConsentSettingsPayload, ConsentSettingsPayload]):
|
|
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)
|
|
|
|
|
|
class EmailSettings:
|
|
"""Represents email communication preferences.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
initialized: :class:`bool`
|
|
Whether the email communication preferences have been initialized.
|
|
communication: :class:`bool`
|
|
Whether you want to receive emails for missed calls/messages.
|
|
social: :class:`bool`
|
|
Whether you want to receive emails for friend requests/suggestions or events.
|
|
recommendations_and_events: :class:`bool`
|
|
Whether you want to receive emails for recommended servers and events.
|
|
tips: :class:`bool`
|
|
Whether you want to receive emails for advice and tricks.
|
|
updates_and_announcements: :class:`bool`
|
|
Whether you want to receive emails for updates and new features.
|
|
family_center_digest: :class:`bool`
|
|
Whether you want to receive weekly emails for recent family activity.
|
|
|
|
.. versionadded:: 2.1
|
|
"""
|
|
|
|
__slots__ = (
|
|
'_state',
|
|
'initialized',
|
|
'communication',
|
|
'social',
|
|
'recommendations_and_events',
|
|
'tips',
|
|
'updates_and_announcements',
|
|
'family_center_digest',
|
|
)
|
|
|
|
def __init__(self, *, data: EmailSettingsPayload, state: ConnectionState):
|
|
self._state = state
|
|
self._update(data)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<EmailSettings initialized={self.initialized}>'
|
|
|
|
def _update(self, data: EmailSettingsPayload):
|
|
self.initialized: bool = data.get('initialized', False)
|
|
categories = data.get('categories', {})
|
|
self.communication: bool = categories.get('communication', False)
|
|
self.social: bool = categories.get('social', False)
|
|
self.recommendations_and_events: bool = categories.get('recommendations_and_events', False)
|
|
self.tips: bool = categories.get('tips', False)
|
|
self.updates_and_announcements: bool = categories.get('updates_and_announcements', False)
|
|
self.family_center_digest: bool = categories.get('family_center_digest', False)
|
|
|
|
@overload
|
|
async def edit(self) -> None:
|
|
...
|
|
|
|
@overload
|
|
async def edit(
|
|
self,
|
|
*,
|
|
initialized: bool = MISSING,
|
|
communication: bool = MISSING,
|
|
social: bool = MISSING,
|
|
recommendations_and_events: bool = MISSING,
|
|
tips: bool = MISSING,
|
|
updates_and_announcements: bool = MISSING,
|
|
) -> None:
|
|
...
|
|
|
|
async def edit(self, **kwargs) -> None:
|
|
"""|coro|
|
|
|
|
Edits the email settings.
|
|
|
|
All parameters are optional.
|
|
|
|
Parameters
|
|
-----------
|
|
communication: :class:`bool`
|
|
Indicates if you want to receive communication emails.
|
|
social: :class:`bool`
|
|
Indicates if you want to receive social emails.
|
|
recommendations_and_events: :class:`bool`
|
|
Indicates if you want to receive recommendations and events emails.
|
|
tips: :class:`bool`
|
|
Indicates if you want to receive tips emails.
|
|
updates_and_announcements: :class:`bool`
|
|
Indicates if you want to receive updates and announcements emails.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Editing the settings failed.
|
|
"""
|
|
payload = {}
|
|
|
|
# It seems that initialized is settable, but it doesn't do anything
|
|
# So we support just in case but leave it undocumented
|
|
initialized = kwargs.pop('initialized', MISSING)
|
|
if initialized is not MISSING:
|
|
payload['initialized'] = initialized
|
|
if kwargs:
|
|
payload['categories'] = kwargs
|
|
|
|
data = await self._state.http.edit_email_settings(**payload)
|
|
self._update(data)
|
|
|