Browse Source

Cleanup and implement missing setting types (fixes #<deleted-issue>)

pull/10109/head
dolfies 3 years ago
parent
commit
b2d834f898
  1. 7
      discord/abc.py
  2. 27
      discord/channel.py
  3. 13
      discord/client.py
  4. 7
      discord/enums.py
  5. 22
      discord/guild.py
  6. 6
      discord/http.py
  7. 471
      discord/settings.py
  8. 32
      discord/state.py
  9. 63
      discord/tracking.py
  10. 10
      discord/user.py
  11. 27
      docs/api.rst

7
discord/abc.py

@ -582,11 +582,14 @@ class GuildChannel:
def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
.. versionadded:: 2.0
"""
guild = self.guild
# guild.notification_settings will always be present at this point
return guild.notification_settings._channel_overrides.get(self.id) or ChannelSettings(guild.id, state=self._state) # type: ignore
return guild.notification_settings._channel_overrides.get(
self.id, self._state.default_channel_settings(guild.id, self.id)
)
@property
def changed_roles(self) -> List[Role]:

27
discord/channel.py

@ -83,6 +83,7 @@ if TYPE_CHECKING:
from .file import File
from .user import ClientUser, User
from .guild import Guild, GuildChannel as GuildChannelType
from .settings import ChannelSettings
from .types.channel import (
TextChannel as TextChannelPayload,
VoiceChannel as VoiceChannelPayload,
@ -2269,6 +2270,19 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
def __repr__(self) -> str:
return f'<DMChannel id={self.id} recipient={self.recipient!r}>'
@property
def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
.. versionadded:: 2.0
"""
state = self._state
return state.client.notification_settings._channel_overrides.get(
self.id, state.default_channel_settings(None, self.id)
)
@property
def call(self) -> Optional[PrivateCall]:
"""Optional[:class:`PrivateCall`]: The channel's currently active call."""
@ -2526,6 +2540,19 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
def __repr__(self) -> str:
return f'<GroupChannel id={self.id} name={self.name!r}>'
@property
def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
.. versionadded:: 2.0
"""
state = self._state
return state.client.notification_settings._channel_overrides.get(
self.id, state.default_channel_settings(None, self.id)
)
@property
def owner(self) -> User:
""":class:`User`: The owner that owns the group channel."""

13
discord/client.py

@ -88,6 +88,7 @@ if TYPE_CHECKING:
from .message import Message
from .member import Member
from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .types.snowflake import Snowflake as _Snowflake
# fmt: off
@ -756,6 +757,18 @@ class Client:
"""Optional[:class:`.VoiceProtocol`]: Returns the :class:`.VoiceProtocol` associated with private calls, if any."""
return self._connection._get_voice_client(self._connection.self_id)
@property
def notification_settings(self) -> GuildSettings:
""":class:`GuildSettings`: Returns the notification settings for private channels.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
.. versionadded:: 2.0
"""
# The private channel pseudo-guild settings have a guild ID of null
state = self._connection
return state.guild_settings.get(None, state.default_guild_settings(None))
@property
def initial_activity(self) -> Optional[ActivityTypes]:
"""Optional[:class:`.BaseActivity`]: The primary activity set upon logging in.

7
discord/enums.py

@ -41,6 +41,7 @@ __all__ = (
'UserFlags',
'ActivityType',
'NotificationLevel',
'HighlightLevel',
'TeamMembershipState',
'WebhookType',
'ExpireBehaviour',
@ -386,6 +387,12 @@ class NotificationLevel(Enum, comparable=True):
return self.value
class HighlightLevel(Enum):
default = 0
disabled = 1
enabled = 2
class AuditLogActionCategory(Enum):
create = 1
delete = 2

22
discord/guild.py

@ -86,7 +86,6 @@ from .sticker import GuildSticker
from .file import File
from .audit_logs import AuditLogEntry
from .object import OLDEST_OBJECT, Object
from .settings import GuildSettings
from .profile import MemberProfile
from .partial_emoji import PartialEmoji
from .welcome_screen import *
@ -119,6 +118,7 @@ if TYPE_CHECKING:
from .webhook import Webhook
from .state import ConnectionState
from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .types.channel import (
GuildChannel as GuildChannelPayload,
TextChannel as TextChannelPayload,
@ -259,10 +259,6 @@ class Guild(Hashable):
premium_progress_bar_enabled: :class:`bool`
Indicates if the guild has premium AKA server boost level progress bar enabled.
.. versionadded:: 2.0
notification_settings: :class:`GuildSettings`
The notification settings for the guild.
.. versionadded:: 2.0
keywords: Optional[:class:`str`]
Discovery search keywords for the guild.
@ -295,7 +291,6 @@ class Guild(Hashable):
'mfa_level',
'vanity_url_code',
'owner_application_id',
'notification_settings',
'command_counts',
'_members',
'_channels',
@ -352,7 +347,6 @@ class Guild(Hashable):
self._stage_instances: Dict[int, StageInstance] = {}
self._scheduled_events: Dict[int, ScheduledEvent] = {}
self._state: ConnectionState = state
self.notification_settings: Optional[GuildSettings] = None
self.command_counts: Optional[CommandCounts] = None
self._member_count: int = 0
self._presence_count: Optional[int] = None
@ -531,9 +525,6 @@ class Guild(Hashable):
large = None if self._member_count == 0 else self._member_count >= 250
self._large: Optional[bool] = guild.get('large', large)
if (settings := guild.get('settings')) is not None:
self.notification_settings = GuildSettings(state=state, data=settings)
if (counts := guild.get('application_command_counts')) is not None:
self.command_counts = CommandCounts(counts.get(0, 0), counts.get(1, 0), counts.get(2, 0))
@ -644,6 +635,17 @@ class Guild(Hashable):
"""Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any."""
return self._state._get_voice_client(self.id)
@property
def notification_settings(self) -> GuildSettings:
""":class:`GuildSettings`: Returns the notification settings for the guild.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
.. versionadded:: 2.0
"""
state = self._state
return state.guild_settings.get(self.id, state.default_guild_settings(self.id))
@property
def text_channels(self) -> List[TextChannel]:
"""List[:class:`TextChannel`]: A list of text channels that belongs to this guild.

6
discord/http.py

@ -2379,6 +2379,12 @@ class HTTPClient:
def edit_tracking(self, payload):
return self.request(Route('POST', '/users/@me/consent'), json=payload)
def get_email_settings(self):
return self.request(Route('GET', '/users/@me/email-settings'))
def edit_email_settings(self, **payload):
return self.request(Route('PATCH', '/users/@me/email-settings'), json={'settings': payload})
def mobile_report( # Report v1
self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str
): # TODO: return type

471
discord/settings.py

@ -24,12 +24,13 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, overload
from .activity import create_settings_activity
from .enums import (
FriendFlags,
HighlightLevel,
Locale,
NotificationLevel,
Status,
@ -39,19 +40,22 @@ from .enums import (
try_enum,
)
from .guild_folder import GuildFolder
from .utils import MISSING, parse_time, utcnow
from .object import Object
from .utils import MISSING, _get_as_snowflake, parse_time, utcnow, find
if TYPE_CHECKING:
from .abc import GuildChannel
from .abc import GuildChannel, PrivateChannel
from .activity import CustomActivity
from .guild import Guild
from .state import ConnectionState
from .tracking import Tracking
from .user import ClientUser
__all__ = (
'ChannelSettings',
'GuildSettings',
'UserSettings',
'TrackingSettings',
'EmailSettings',
'MuteConfig',
)
@ -105,9 +109,13 @@ class UserSettings:
show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing.
stream_notifications_enabled: :class:`bool`
Unknown.
Whether stream notifications for friends will be received.
timezone_offset: :class:`int`
The timezone offset to use.
view_nsfw_commands: :class:`bool`
Whether or not to show NSFW application commands.
.. versionadded:: 2.0
view_nsfw_guilds: :class:`bool`
Whether or not to show NSFW guilds on iOS.
"""
@ -133,6 +141,7 @@ class UserSettings:
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:
@ -142,8 +151,8 @@ class UserSettings:
def __repr__(self) -> str:
return '<Settings>'
def _get_guild(self, id: int) -> Optional[Guild]:
return self._state._get_guild(int(id))
def _get_guild(self, id: int) -> Guild:
return self._state._get_guild(int(id)) or Object(id=int(id)) # type: ignore # Lying for better developer UX
def _update(self, data: Dict[str, Any]) -> None:
RAW_VALUES = {
@ -167,6 +176,7 @@ class UserSettings:
'show_current_game',
'stream_notifications_enabled',
'timezone_offset',
'view_nsfw_commands',
'view_nsfw_guilds',
}
@ -186,6 +196,14 @@ class UserSettings:
Parameters
----------
activity_restricted_guilds: List[:class:`abc.Snowflake`]
A list of guilds that your current activity will not be shown in.
.. versionadded:: 2.0
activity_joining_restricted_guilds: List[:class:`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 your mobile device.
@ -234,7 +252,7 @@ class UserSettings:
Whether or not to enable the new Discord mobile phone number friend
requesting features.
passwordless: :class:`bool`
Unknown.
Whether the account is passwordless.
render_embeds: :class:`bool`
Whether or not to render embeds that are sent in the chat.
render_reactions: :class:`bool`
@ -244,11 +262,15 @@ class UserSettings:
show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing.
stream_notifications_enabled: :class:`bool`
Unknown.
Whether stream notifications for friends will be received.
theme: :class:`Theme`
The theme of the Discord UI.
timezone_offset: :class:`int`
The timezone offset to use.
view_nsfw_commands: :class:`bool`
Whether or not to show NSFW application commands.
.. versionadded:: 2.0
view_nsfw_guilds: :class:`bool`
Whether or not to show NSFW guilds on iOS.
@ -264,10 +286,32 @@ class UserSettings:
"""
return await self._state.user.edit_settings(**kwargs) # type: ignore
async def fetch_tracking(self) -> Tracking:
async def email_settings(self) -> EmailSettings:
"""|coro|
Retrieves your :class:`Tracking` settings.
Retrieves your :class:`EmailSettings`.
.. versionadded:: 2.0
Raises
-------
HTTPException
Getting the email settings failed.
Returns
-------
:class:`.EmailSettings`
The email settings.
"""
data = await self._state.http.get_email_settings()
return EmailSettings(data=data, state=self._state)
async def fetch_tracking_settings(self) -> TrackingSettings:
"""|coro|
Retrieves your :class:`TrackingSettings`.
.. versionadded:: 2.0
Raises
------
@ -280,13 +324,32 @@ class UserSettings:
The tracking settings.
"""
data = await self._state.http.get_tracking()
return Tracking(state=self._state, data=data)
return TrackingSettings(state=self._state, data=data)
@property
def tracking(self) -> Optional[Tracking]:
"""Optional[:class:`Tracking`]: Returns your tracking settings if available."""
def tracking_settings(self) -> Optional[TrackingSettings]:
"""Optional[:class:`TrackingSettings`]: Returns your tracking settings if available.
.. versionadded:: 2.0
"""
return self._state.consents
@property
def activity_restricted_guilds(self) -> List[Guild]:
"""List[:class:`abc.Snowflake`]: 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:`abc.Snowflake`]: 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 or not to animate stickers in the chat."""
@ -316,17 +379,21 @@ class UserSettings:
@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(filter(None, map(self._get_guild, getattr(self, '_guild_positions', []))))
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."""
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`: Unknown."""
""":class:`bool`: Whether the account is passwordless."""
return getattr(self, '_passwordless', False)
@property
@ -386,13 +453,7 @@ class MuteConfig:
self.muted: bool = muted
self.until: Optional[datetime] = until
for item in {'__bool__', '__eq__', '__float__', '__int__', '__str__'}:
setattr(self, item, getattr(muted, item))
def __repr__(self) -> str:
return f'<MuteConfig muted={self.muted} until={self.until}>'
def __str__(self) -> str:
return str(self.muted)
def __int__(self) -> int:
@ -411,6 +472,8 @@ class MuteConfig:
class ChannelSettings:
"""Represents a channel's notification settings.
.. versionadded:: 2.0
Attributes
----------
level: :class:`NotificationLevel`
@ -418,7 +481,8 @@ class ChannelSettings:
muted: :class:`MuteConfig`
The mute configuration for the channel.
collapsed: :class:`bool`
Unknown.
Whether the channel is collapsed.
Only applicable to channels of type :attr:`ChannelType.category`.
"""
if TYPE_CHECKING:
@ -427,12 +491,17 @@ class ChannelSettings:
muted: MuteConfig
collapsed: bool
def __init__(self, guild_id, *, data: Dict[str, Any] = {}, state: ConnectionState) -> None:
self._guild_id: int = guild_id
def __init__(self, guild_id: Optional[int] = None, *, data: Dict[str, Any], 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: Dict[str, Any]) -> 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)
@ -440,19 +509,24 @@ class ChannelSettings:
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {})
@property
def channel(self) -> Optional[GuildChannel]:
"""Optional[:class:`.abc.GuildChannel`]: Returns the channel these settings are for."""
def channel(self) -> Union[GuildChannel, PrivateChannel]:
"""Union[:class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`]: Returns the channel these settings are for."""
guild = self._state._get_guild(self._guild_id)
return guild and guild.get_channel(self._channel_id)
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: bool = MISSING,
duration: Optional[int] = MISSING,
muted_until: Optional[Union[bool, datetime]] = MISSING,
collapsed: bool = MISSING,
level: NotificationLevel = MISSING,
) -> Optional[ChannelSettings]:
) -> ChannelSettings:
"""|coro|
Edits the channel's notification settings.
@ -461,13 +535,14 @@ class ChannelSettings:
Parameters
-----------
muted: :class:`bool`
Indicates if the channel should be muted or not.
duration: Optional[Union[:class:`int`, :class:`float`]]
The amount of time in hours that the channel should be muted for.
Defaults to indefinite.
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`
Unknown.
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.
@ -481,21 +556,29 @@ class ChannelSettings:
:class:`ChannelSettings`
The new notification settings.
"""
state = self._state
guild_id = self._guild_id
channel_id = self._channel_id
payload = {}
if muted is not MISSING:
payload['muted'] = muted
if duration is not MISSING:
if muted is MISSING:
if muted_until is not MISSING:
if not muted_until:
payload['muted'] = False
else:
payload['muted'] = True
if duration is not None:
mute_config = {
'selected_time_window': duration * 3600,
'end_time': (datetime.utcnow() + timedelta(hours=duration)).isoformat(),
}
payload['mute_config'] = mute_config
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
@ -503,15 +586,20 @@ class ChannelSettings:
if level is not MISSING:
payload['message_notifications'] = level.value
fields = {'channel_overrides': {str(self._channel_id): payload}}
data = await self._state.http.edit_guild_settings(self._guild_id, fields)
fields = {'channel_overrides': {str(channel_id): payload}}
data = await state.http.edit_guild_settings(guild_id or '@me', fields)
return ChannelSettings(self._guild_id, data=data['channel_overrides'][str(self._channel_id)], state=self._state)
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)
class GuildSettings:
"""Represents a guild's notification settings.
.. versionadded:: 2.0
Attributes
----------
level: :class:`NotificationLevel`
@ -524,36 +612,49 @@ class GuildSettings:
Whether to suppress role notifications.
hide_muted_channels: :class:`bool`
Whether to hide muted channels.
mobile_push_notifications: :class:`bool`
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: int
version: int
_guild_id: Optional[int]
level: NotificationLevel
muted: MuteConfig
suppress_everyone: bool
suppress_roles: bool
hide_muted_channels: bool
mobile_push_notifications: bool
level: NotificationLevel
mobile_push: bool
mute_scheduled_events: bool
notify_highlights: HighlightLevel
version: int
def __init__(self, *, data: Dict[str, Any], state: ConnectionState) -> None:
self._state = state
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: Dict[str, Any]) -> None:
self._guild_id = guild_id = int(data['guild_id'])
self.version = data.get('version', -1) # Overriden by real data
# 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_notifications = data.get('mobile_push', True)
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', -1) # Overriden by real data
self.level = try_enum(NotificationLevel, data.get('message_notifications', 3))
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {})
self._channel_overrides = overrides = {}
state = self._state
@ -562,9 +663,14 @@ class GuildSettings:
overrides[channel_id] = ChannelSettings(guild_id, data=override, state=state)
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: Returns the guild that these settings are for."""
return self._state._get_guild(self._guild_id)
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_guild(self._guild_id) or Object(id=self._guild_id) # type: ignore # Lying for better developer UX
return self._state.user # type: ignore # Should always be present here
@property
def channel_overrides(self) -> List[ChannelSettings]:
@ -573,13 +679,14 @@ class GuildSettings:
async def edit(
self,
muted: bool = MISSING,
duration: Optional[int] = MISSING,
muted_until: Optional[Union[bool, datetime]] = MISSING,
level: NotificationLevel = MISSING,
suppress_everyone: bool = MISSING,
suppress_roles: bool = MISSING,
mobile_push_notifications: bool = MISSING,
mobile_push: bool = MISSING,
hide_muted_channels: bool = MISSING,
mute_scheduled_events: bool = MISSING,
notify_highlights: HighlightLevel = MISSING,
) -> Optional[GuildSettings]:
"""|coro|
@ -589,21 +696,25 @@ class GuildSettings:
Parameters
-----------
muted: :class:`bool`
Indicates if the guild should be muted or not.
duration: Optional[Union[:class:`int`, :class:`float`]]
The amount of time in hours that the guild should be muted for.
Defaults to indefinite.
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_notifications: :class:`bool`
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
-------
@ -617,19 +728,24 @@ class GuildSettings:
"""
payload = {}
if muted is not MISSING:
payload['muted'] = muted
if duration is not MISSING:
if muted is MISSING:
if muted_until is not MISSING:
if not muted_until:
payload['muted'] = False
else:
payload['muted'] = True
if duration is not None:
mute_config = {
'selected_time_window': duration * 3600,
'end_time': (datetime.utcnow() + timedelta(hours=duration)).isoformat(),
}
payload['mute_config'] = mute_config
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 level is not MISSING:
payload['message_notifications'] = level.value
@ -640,12 +756,187 @@ class GuildSettings:
if suppress_roles is not MISSING:
payload['suppress_roles'] = suppress_roles
if mobile_push_notifications is not MISSING:
payload['mobile_push'] = mobile_push_notifications
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
data = await self._state.http.edit_guild_settings(self._guild_id, payload)
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: Dict[str, Dict[str, bool]], 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: 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)
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.
"""
__slots__ = (
'_state',
'initialized',
'communication',
'social',
'recommendations_and_events',
'tips',
'updates_and_announcements',
)
def __init__(self, *, data: dict, state: ConnectionState):
self._state = state
self._update(data)
def __repr__(self) -> str:
return f'<EmailSettings initialized={self.initialized}>'
def _update(self, data: dict):
self.initialized = data.get('initialized', False)
categories = data.get('categories', {})
self.communication = categories.get('communication', False)
self.social = categories.get('social', False)
self.recommendations_and_events = categories.get('recommendations_and_events', False)
self.tips = categories.get('tips', False)
self.updates_and_announcements = categories.get('updates_and_announcements', False)
@overload
async def edit(self) -> None:
...
@overload
async def edit(
self,
*,
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', None)
if initialized is not None:
payload['initialized'] = initialized
if kwargs:
payload['categories'] = kwargs
data = await self._state.http.edit_email_settings(**payload)
self._update(data)

32
discord/state.py

@ -71,8 +71,7 @@ from .scheduled_event import ScheduledEvent
from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
from .sticker import GuildSticker
from .settings import UserSettings, GuildSettings
from .tracking import Tracking
from .settings import UserSettings, GuildSettings, ChannelSettings, TrackingSettings
from .interactions import Interaction
from .permissions import Permissions, PermissionOverwrite
from .member import _ClientStatus
@ -457,7 +456,8 @@ class ConnectionState:
self.user: Optional[ClientUser] = None
self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary()
self.settings: Optional[UserSettings] = None
self.consents: Optional[Tracking] = None
self.guild_settings: Dict[Optional[int], GuildSettings] = {}
self.consents: Optional[TrackingSettings] = None
self.connections: Dict[str, Connection] = {}
self.analytics_token: Optional[str] = None
self.preferred_regions: List[str] = []
@ -817,7 +817,6 @@ class ConnectionState:
self.clear()
data = self._ready_data
guild_settings = data.get('user_guild_settings', {}).get('entries', [])
# Temp user parsing
temp_users: Dict[int, UserPayload] = {int(data['user']['id']): data['user']}
@ -833,11 +832,6 @@ class ConnectionState:
data.get('merged_members', []),
extra_data['merged_presences'].get('guilds', []),
):
guild_data['settings'] = utils.find( # type: ignore # This key does not actually exist in the payload
lambda i: i['guild_id'] == guild_data['id'],
guild_settings,
) or {'guild_id': guild_data['id']}
for presence in merged_presences:
presence['user'] = {'id': presence['user_id']} # type: ignore # :(
@ -892,7 +886,11 @@ class ConnectionState:
self.analytics_token = data.get('analytics_token')
self.preferred_regions = data.get('geo_ordered_rtc_regions', ['us-central'])
self.settings = UserSettings(data=data.get('user_settings', {}), state=self)
self.consents = Tracking(data=data.get('consents', {}), state=self)
self.guild_settings = {
utils._get_as_snowflake(entry, 'guild_id'): GuildSettings(data=entry, state=self)
for entry in data.get('user_guild_settings', {}).get('entries', [])
}
self.consents = TrackingSettings(data=data.get('consents', {}), state=self)
self.country_code = data.get('country_code', 'US')
self.session_type = data.get('session_type', 'normal')
self.connections = {c['id']: Connection(state=self, data=c) for c in data.get('connected_accounts', [])}
@ -1079,13 +1077,9 @@ class ConnectionState:
self.dispatch('internal_settings_update', old_settings, new_settings)
def parse_user_guild_settings_update(self, data) -> None:
guild_id = int(data['guild_id'])
guild = self._get_guild(guild_id)
if guild is None:
_log.debug('USER_GUILD_SETTINGS_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
return
guild_id = utils._get_as_snowflake(data, 'guild_id')
settings = guild.notification_settings
settings = self.guild_settings.get(guild_id)
if settings is not None:
old_settings = copy.copy(settings)
settings._update(data)
@ -2316,3 +2310,9 @@ class ConnectionState:
def create_interaction_application(self, data: dict) -> InteractionApplication:
return InteractionApplication(state=self, data=data)
def default_guild_settings(self, guild_id: Optional[int]) -> GuildSettings:
return GuildSettings(data={'guild_id': guild_id}, state=self)
def default_channel_settings(self, guild_id: Optional[int], channel_id: int) -> ChannelSettings:
return ChannelSettings(guild_id, data={'channel_id': channel_id}, state=self)

63
discord/tracking.py

@ -28,7 +28,7 @@ from base64 import b64encode
import json
from random import choice
from typing import Dict, overload, Optional, TYPE_CHECKING
from typing import Dict, Optional, TYPE_CHECKING
from .utils import MISSING
@ -37,12 +37,12 @@ if TYPE_CHECKING:
from .enums import ChannelType
from .types.snowflake import Snowflake
from .state import ConnectionState
# fmt: off
__all__ = (
'ContextProperties',
'Tracking',
)
# fmt: on
class ContextProperties: # Thank you Discord-S.C.U.M
@ -281,60 +281,3 @@ class ContextProperties: # Thank you Discord-S.C.U.M
def __hash__(self) -> int:
return hash(self.value)
class Tracking:
"""Represents your Discord tracking settings.
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: 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)

10
discord/user.py

@ -856,6 +856,16 @@ class ClientUser(BaseUser):
restricted_guilds = [str(x.id) for x in restricted_guilds]
payload['restricted_guilds'] = restricted_guilds
activity_restricted_guilds = kwargs.pop('activity_restricted_guilds', None)
if activity_restricted_guilds:
activity_restricted_guilds = [str(x.id) for x in activity_restricted_guilds]
payload['activity_restricted_guild_ids'] = activity_restricted_guilds
activity_joining_restricted_guilds = kwargs.pop('activity_joining_restricted_guilds', None)
if activity_joining_restricted_guilds:
activity_joining_restricted_guilds = [str(x.id) for x in activity_joining_restricted_guilds]
payload['activity_joining_restricted_guild_ids'] = activity_joining_restricted_guilds
status = kwargs.pop('status', None)
if status:
payload['status'] = status.value

27
docs/api.rst

@ -1623,6 +1623,24 @@ of :class:`enum.Enum`.
Members receive notifications for messages they are mentioned in.
.. class:: HighlightLevel
Specifies whether a :class:`Guild` has highlights included in notifications.
.. versionadded:: 2.0
.. attribute:: default
The highlight level is set to Discord default.
This seems to always be enabled, which makes the purpose of this enum unclear.
.. attribute:: disabled
Members do not receive additional notifications for highlights.
.. attribute:: enabled
Members receive additional notifications for highlights.
.. class:: ContentFilter
Specifies a :class:`Guild`\'s explicit content filter, which is the machine
@ -4303,9 +4321,14 @@ Settings
.. autoclass:: ChannelSettings()
:members:
.. attributetable:: Tracking
.. attributetable:: TrackingSettings
.. autoclass:: TrackingSettings()
:members:
.. attributetable:: EmailSettings
.. autoclass:: Tracking()
.. autoclass:: EmailSettings()
:members:
.. attributetable:: GuildFolder

Loading…
Cancel
Save