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: def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel. """: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 .. versionadded:: 2.0
""" """
guild = self.guild guild = self.guild
# guild.notification_settings will always be present at this point return guild.notification_settings._channel_overrides.get(
return guild.notification_settings._channel_overrides.get(self.id) or ChannelSettings(guild.id, state=self._state) # type: ignore self.id, self._state.default_channel_settings(guild.id, self.id)
)
@property @property
def changed_roles(self) -> List[Role]: def changed_roles(self) -> List[Role]:

27
discord/channel.py

@ -83,6 +83,7 @@ if TYPE_CHECKING:
from .file import File from .file import File
from .user import ClientUser, User from .user import ClientUser, User
from .guild import Guild, GuildChannel as GuildChannelType from .guild import Guild, GuildChannel as GuildChannelType
from .settings import ChannelSettings
from .types.channel import ( from .types.channel import (
TextChannel as TextChannelPayload, TextChannel as TextChannelPayload,
VoiceChannel as VoiceChannelPayload, VoiceChannel as VoiceChannelPayload,
@ -2269,6 +2270,19 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<DMChannel id={self.id} recipient={self.recipient!r}>' 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 @property
def call(self) -> Optional[PrivateCall]: def call(self) -> Optional[PrivateCall]:
"""Optional[:class:`PrivateCall`]: The channel's currently active call.""" """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: def __repr__(self) -> str:
return f'<GroupChannel id={self.id} name={self.name!r}>' 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 @property
def owner(self) -> User: def owner(self) -> User:
""":class:`User`: The owner that owns the group channel.""" """: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 .message import Message
from .member import Member from .member import Member
from .voice_client import VoiceProtocol from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .types.snowflake import Snowflake as _Snowflake from .types.snowflake import Snowflake as _Snowflake
# fmt: off # fmt: off
@ -756,6 +757,18 @@ class Client:
"""Optional[:class:`.VoiceProtocol`]: Returns the :class:`.VoiceProtocol` associated with private calls, if any.""" """Optional[:class:`.VoiceProtocol`]: Returns the :class:`.VoiceProtocol` associated with private calls, if any."""
return self._connection._get_voice_client(self._connection.self_id) 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 @property
def initial_activity(self) -> Optional[ActivityTypes]: def initial_activity(self) -> Optional[ActivityTypes]:
"""Optional[:class:`.BaseActivity`]: The primary activity set upon logging in. """Optional[:class:`.BaseActivity`]: The primary activity set upon logging in.

7
discord/enums.py

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

22
discord/guild.py

@ -86,7 +86,6 @@ from .sticker import GuildSticker
from .file import File from .file import File
from .audit_logs import AuditLogEntry from .audit_logs import AuditLogEntry
from .object import OLDEST_OBJECT, Object from .object import OLDEST_OBJECT, Object
from .settings import GuildSettings
from .profile import MemberProfile from .profile import MemberProfile
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .welcome_screen import * from .welcome_screen import *
@ -119,6 +118,7 @@ if TYPE_CHECKING:
from .webhook import Webhook from .webhook import Webhook
from .state import ConnectionState from .state import ConnectionState
from .voice_client import VoiceProtocol from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .types.channel import ( from .types.channel import (
GuildChannel as GuildChannelPayload, GuildChannel as GuildChannelPayload,
TextChannel as TextChannelPayload, TextChannel as TextChannelPayload,
@ -259,10 +259,6 @@ class Guild(Hashable):
premium_progress_bar_enabled: :class:`bool` premium_progress_bar_enabled: :class:`bool`
Indicates if the guild has premium AKA server boost level progress bar enabled. 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 .. versionadded:: 2.0
keywords: Optional[:class:`str`] keywords: Optional[:class:`str`]
Discovery search keywords for the guild. Discovery search keywords for the guild.
@ -295,7 +291,6 @@ class Guild(Hashable):
'mfa_level', 'mfa_level',
'vanity_url_code', 'vanity_url_code',
'owner_application_id', 'owner_application_id',
'notification_settings',
'command_counts', 'command_counts',
'_members', '_members',
'_channels', '_channels',
@ -352,7 +347,6 @@ class Guild(Hashable):
self._stage_instances: Dict[int, StageInstance] = {} self._stage_instances: Dict[int, StageInstance] = {}
self._scheduled_events: Dict[int, ScheduledEvent] = {} self._scheduled_events: Dict[int, ScheduledEvent] = {}
self._state: ConnectionState = state self._state: ConnectionState = state
self.notification_settings: Optional[GuildSettings] = None
self.command_counts: Optional[CommandCounts] = None self.command_counts: Optional[CommandCounts] = None
self._member_count: int = 0 self._member_count: int = 0
self._presence_count: Optional[int] = None 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 large = None if self._member_count == 0 else self._member_count >= 250
self._large: Optional[bool] = guild.get('large', large) 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: 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)) 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.""" """Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any."""
return self._state._get_voice_client(self.id) 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 @property
def text_channels(self) -> List[TextChannel]: def text_channels(self) -> List[TextChannel]:
"""List[:class:`TextChannel`]: A list of text channels that belongs to this guild. """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): def edit_tracking(self, payload):
return self.request(Route('POST', '/users/@me/consent'), json=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 def mobile_report( # Report v1
self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str
): # TODO: return type ): # TODO: return type

471
discord/settings.py

@ -24,12 +24,13 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime
from typing import Any, Dict, List, Optional, TYPE_CHECKING from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, overload
from .activity import create_settings_activity from .activity import create_settings_activity
from .enums import ( from .enums import (
FriendFlags, FriendFlags,
HighlightLevel,
Locale, Locale,
NotificationLevel, NotificationLevel,
Status, Status,
@ -39,19 +40,22 @@ from .enums import (
try_enum, try_enum,
) )
from .guild_folder import GuildFolder 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: if TYPE_CHECKING:
from .abc import GuildChannel from .abc import GuildChannel, PrivateChannel
from .activity import CustomActivity from .activity import CustomActivity
from .guild import Guild from .guild import Guild
from .state import ConnectionState from .state import ConnectionState
from .tracking import Tracking from .user import ClientUser
__all__ = ( __all__ = (
'ChannelSettings', 'ChannelSettings',
'GuildSettings', 'GuildSettings',
'UserSettings', 'UserSettings',
'TrackingSettings',
'EmailSettings',
'MuteConfig', 'MuteConfig',
) )
@ -105,9 +109,13 @@ class UserSettings:
show_current_game: :class:`bool` show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing. Whether or not to display the game that you are currently playing.
stream_notifications_enabled: :class:`bool` stream_notifications_enabled: :class:`bool`
Unknown. Whether stream notifications for friends will be received.
timezone_offset: :class:`int` timezone_offset: :class:`int`
The timezone offset to use. 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` view_nsfw_guilds: :class:`bool`
Whether or not to show NSFW guilds on iOS. Whether or not to show NSFW guilds on iOS.
""" """
@ -133,6 +141,7 @@ class UserSettings:
show_current_game: bool show_current_game: bool
stream_notifications_enabled: bool stream_notifications_enabled: bool
timezone_offset: int timezone_offset: int
view_nsfw_commands: bool
view_nsfw_guilds: bool view_nsfw_guilds: bool
def __init__(self, *, data, state: ConnectionState) -> None: def __init__(self, *, data, state: ConnectionState) -> None:
@ -142,8 +151,8 @@ class UserSettings:
def __repr__(self) -> str: def __repr__(self) -> str:
return '<Settings>' return '<Settings>'
def _get_guild(self, id: int) -> Optional[Guild]: def _get_guild(self, id: int) -> Guild:
return self._state._get_guild(int(id)) 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: def _update(self, data: Dict[str, Any]) -> None:
RAW_VALUES = { RAW_VALUES = {
@ -167,6 +176,7 @@ class UserSettings:
'show_current_game', 'show_current_game',
'stream_notifications_enabled', 'stream_notifications_enabled',
'timezone_offset', 'timezone_offset',
'view_nsfw_commands',
'view_nsfw_guilds', 'view_nsfw_guilds',
} }
@ -186,6 +196,14 @@ class UserSettings:
Parameters 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` afk_timeout: :class:`int`
How long (in seconds) the user needs to be AFK until Discord How long (in seconds) the user needs to be AFK until Discord
sends push notifications to your mobile device. 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 Whether or not to enable the new Discord mobile phone number friend
requesting features. requesting features.
passwordless: :class:`bool` passwordless: :class:`bool`
Unknown. Whether the account is passwordless.
render_embeds: :class:`bool` render_embeds: :class:`bool`
Whether or not to render embeds that are sent in the chat. Whether or not to render embeds that are sent in the chat.
render_reactions: :class:`bool` render_reactions: :class:`bool`
@ -244,11 +262,15 @@ class UserSettings:
show_current_game: :class:`bool` show_current_game: :class:`bool`
Whether or not to display the game that you are currently playing. Whether or not to display the game that you are currently playing.
stream_notifications_enabled: :class:`bool` stream_notifications_enabled: :class:`bool`
Unknown. Whether stream notifications for friends will be received.
theme: :class:`Theme` theme: :class:`Theme`
The theme of the Discord UI. The theme of the Discord UI.
timezone_offset: :class:`int` timezone_offset: :class:`int`
The timezone offset to use. 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` view_nsfw_guilds: :class:`bool`
Whether or not to show NSFW guilds on iOS. 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 return await self._state.user.edit_settings(**kwargs) # type: ignore
async def fetch_tracking(self) -> Tracking: async def email_settings(self) -> EmailSettings:
"""|coro| """|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 Raises
------ ------
@ -280,13 +324,32 @@ class UserSettings:
The tracking settings. The tracking settings.
""" """
data = await self._state.http.get_tracking() data = await self._state.http.get_tracking()
return Tracking(state=self._state, data=data) return TrackingSettings(state=self._state, data=data)
@property @property
def tracking(self) -> Optional[Tracking]: def tracking_settings(self) -> Optional[TrackingSettings]:
"""Optional[:class:`Tracking`]: Returns your tracking settings if available.""" """Optional[:class:`TrackingSettings`]: Returns your tracking settings if available.
.. versionadded:: 2.0
"""
return self._state.consents 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 @property
def animate_stickers(self) -> StickerAnimationOptions: def animate_stickers(self) -> StickerAnimationOptions:
""":class:`StickerAnimationOptions`: Whether or not to animate stickers in the chat.""" """:class:`StickerAnimationOptions`: Whether or not to animate stickers in the chat."""
@ -316,17 +379,21 @@ class UserSettings:
@property @property
def guild_positions(self) -> List[Guild]: 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.""" """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 @property
def locale(self) -> Locale: def locale(self) -> Locale:
""":class:`Locale`: The :rfc:`3066` language identifier """: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')) return try_enum(Locale, getattr(self, '_locale', 'en-US'))
@property @property
def passwordless(self) -> bool: def passwordless(self) -> bool:
""":class:`bool`: Unknown.""" """:class:`bool`: Whether the account is passwordless."""
return getattr(self, '_passwordless', False) return getattr(self, '_passwordless', False)
@property @property
@ -386,13 +453,7 @@ class MuteConfig:
self.muted: bool = muted self.muted: bool = muted
self.until: Optional[datetime] = until self.until: Optional[datetime] = until
for item in {'__bool__', '__eq__', '__float__', '__int__', '__str__'}:
setattr(self, item, getattr(muted, item))
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<MuteConfig muted={self.muted} until={self.until}>'
def __str__(self) -> str:
return str(self.muted) return str(self.muted)
def __int__(self) -> int: def __int__(self) -> int:
@ -411,6 +472,8 @@ class MuteConfig:
class ChannelSettings: class ChannelSettings:
"""Represents a channel's notification settings. """Represents a channel's notification settings.
.. versionadded:: 2.0
Attributes Attributes
---------- ----------
level: :class:`NotificationLevel` level: :class:`NotificationLevel`
@ -418,7 +481,8 @@ class ChannelSettings:
muted: :class:`MuteConfig` muted: :class:`MuteConfig`
The mute configuration for the channel. The mute configuration for the channel.
collapsed: :class:`bool` collapsed: :class:`bool`
Unknown. Whether the channel is collapsed.
Only applicable to channels of type :attr:`ChannelType.category`.
""" """
if TYPE_CHECKING: if TYPE_CHECKING:
@ -427,12 +491,17 @@ class ChannelSettings:
muted: MuteConfig muted: MuteConfig
collapsed: bool collapsed: bool
def __init__(self, guild_id, *, data: Dict[str, Any] = {}, state: ConnectionState) -> None: def __init__(self, guild_id: Optional[int] = None, *, data: Dict[str, Any], state: ConnectionState) -> None:
self._guild_id: int = guild_id self._guild_id = guild_id
self._state = state self._state = state
self._update(data) 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: 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._channel_id = int(data['channel_id'])
self.collapsed = data.get('collapsed', False) self.collapsed = data.get('collapsed', False)
@ -440,19 +509,24 @@ class ChannelSettings:
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {}) self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {})
@property @property
def channel(self) -> Optional[GuildChannel]: def channel(self) -> Union[GuildChannel, PrivateChannel]:
"""Optional[:class:`.abc.GuildChannel`]: Returns the channel these settings are for.""" """Union[:class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`]: Returns the channel these settings are for."""
guild = self._state._get_guild(self._guild_id) 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( async def edit(
self, self,
*, *,
muted: bool = MISSING, muted_until: Optional[Union[bool, datetime]] = MISSING,
duration: Optional[int] = MISSING,
collapsed: bool = MISSING, collapsed: bool = MISSING,
level: NotificationLevel = MISSING, level: NotificationLevel = MISSING,
) -> Optional[ChannelSettings]: ) -> ChannelSettings:
"""|coro| """|coro|
Edits the channel's notification settings. Edits the channel's notification settings.
@ -461,13 +535,14 @@ class ChannelSettings:
Parameters Parameters
----------- -----------
muted: :class:`bool` muted_until: Optional[Union[:class:`datetime.datetime`, :class:`bool`]]
Indicates if the channel should be muted or not. The date this channel's mute should expire.
duration: Optional[Union[:class:`int`, :class:`float`]] This can be ``True`` to mute indefinitely, or ``False``/``None`` to unmute.
The amount of time in hours that the channel should be muted for.
Defaults to indefinite. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
collapsed: :class:`bool` 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` level: :class:`NotificationLevel`
Determines what level of notifications you receive for the channel. Determines what level of notifications you receive for the channel.
@ -481,21 +556,29 @@ class ChannelSettings:
:class:`ChannelSettings` :class:`ChannelSettings`
The new notification settings. The new notification settings.
""" """
state = self._state
guild_id = self._guild_id
channel_id = self._channel_id
payload = {} payload = {}
if muted is not MISSING: if muted_until is not MISSING:
payload['muted'] = muted if not muted_until:
payload['muted'] = False
if duration is not MISSING: else:
if muted is MISSING:
payload['muted'] = True payload['muted'] = True
if muted_until is True:
if duration is not None: payload['mute_config'] = {'selected_time_window': -1, 'end_time': None}
mute_config = { else:
'selected_time_window': duration * 3600, if muted_until.tzinfo is None:
'end_time': (datetime.utcnow() + timedelta(hours=duration)).isoformat(), raise TypeError(
} 'muted_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
payload['mute_config'] = mute_config )
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: if collapsed is not MISSING:
payload['collapsed'] = collapsed payload['collapsed'] = collapsed
@ -503,15 +586,20 @@ class ChannelSettings:
if level is not MISSING: if level is not MISSING:
payload['message_notifications'] = level.value payload['message_notifications'] = level.value
fields = {'channel_overrides': {str(self._channel_id): payload}} fields = {'channel_overrides': {str(channel_id): payload}}
data = await self._state.http.edit_guild_settings(self._guild_id, fields) 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: class GuildSettings:
"""Represents a guild's notification settings. """Represents a guild's notification settings.
.. versionadded:: 2.0
Attributes Attributes
---------- ----------
level: :class:`NotificationLevel` level: :class:`NotificationLevel`
@ -524,36 +612,49 @@ class GuildSettings:
Whether to suppress role notifications. Whether to suppress role notifications.
hide_muted_channels: :class:`bool` hide_muted_channels: :class:`bool`
Whether to hide muted channels. Whether to hide muted channels.
mobile_push_notifications: :class:`bool` mobile_push: :class:`bool`
Whether to enable mobile push notifications. 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` version: :class:`int`
The version of the guild's settings. The version of the guild's settings.
""" """
if TYPE_CHECKING: if TYPE_CHECKING:
_channel_overrides: Dict[int, ChannelSettings] _channel_overrides: Dict[int, ChannelSettings]
_guild_id: int _guild_id: Optional[int]
version: int level: NotificationLevel
muted: MuteConfig muted: MuteConfig
suppress_everyone: bool suppress_everyone: bool
suppress_roles: bool suppress_roles: bool
hide_muted_channels: bool hide_muted_channels: bool
mobile_push_notifications: bool mobile_push: bool
level: NotificationLevel mute_scheduled_events: bool
notify_highlights: HighlightLevel
version: int
def __init__(self, *, data: Dict[str, Any], state: ConnectionState) -> None: def __init__(self, *, data: Dict[str, Any], state: ConnectionState) -> None:
self._state = state self._state = state
self._update(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: Dict[str, Any]) -> None: def _update(self, data: Dict[str, Any]) -> None:
self._guild_id = guild_id = int(data['guild_id']) # We consider everything optional because this class can be constructed with no data
self.version = data.get('version', -1) # Overriden by real 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_everyone = data.get('suppress_everyone', False)
self.suppress_roles = data.get('suppress_roles', False) self.suppress_roles = data.get('suppress_roles', False)
self.hide_muted_channels = data.get('hide_muted_channels', 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.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {})
self._channel_overrides = overrides = {} self._channel_overrides = overrides = {}
state = self._state state = self._state
@ -562,9 +663,14 @@ class GuildSettings:
overrides[channel_id] = ChannelSettings(guild_id, data=override, state=state) overrides[channel_id] = ChannelSettings(guild_id, data=override, state=state)
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Union[Guild, ClientUser]:
"""Optional[:class:`Guild`]: Returns the guild that these settings are for.""" """Union[:class:`Guild`, :class:`ClientUser`]: Returns the guild that these settings are for.
return self._state._get_guild(self._guild_id)
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 @property
def channel_overrides(self) -> List[ChannelSettings]: def channel_overrides(self) -> List[ChannelSettings]:
@ -573,13 +679,14 @@ class GuildSettings:
async def edit( async def edit(
self, self,
muted: bool = MISSING, muted_until: Optional[Union[bool, datetime]] = MISSING,
duration: Optional[int] = MISSING,
level: NotificationLevel = MISSING, level: NotificationLevel = MISSING,
suppress_everyone: bool = MISSING, suppress_everyone: bool = MISSING,
suppress_roles: bool = MISSING, suppress_roles: bool = MISSING,
mobile_push_notifications: bool = MISSING, mobile_push: bool = MISSING,
hide_muted_channels: bool = MISSING, hide_muted_channels: bool = MISSING,
mute_scheduled_events: bool = MISSING,
notify_highlights: HighlightLevel = MISSING,
) -> Optional[GuildSettings]: ) -> Optional[GuildSettings]:
"""|coro| """|coro|
@ -589,21 +696,25 @@ class GuildSettings:
Parameters Parameters
----------- -----------
muted: :class:`bool` muted_until: Optional[Union[:class:`datetime.datetime`, :class:`bool`]]
Indicates if the guild should be muted or not. The date this guild's mute should expire.
duration: Optional[Union[:class:`int`, :class:`float`]] This can be ``True`` to mute indefinitely, or ``False``/``None`` to unmute.
The amount of time in hours that the guild should be muted for.
Defaults to indefinite. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
level: :class:`NotificationLevel` level: :class:`NotificationLevel`
Determines what level of notifications you receive for the guild. Determines what level of notifications you receive for the guild.
suppress_everyone: :class:`bool` suppress_everyone: :class:`bool`
Indicates if @everyone mentions should be suppressed for the guild. Indicates if @everyone mentions should be suppressed for the guild.
suppress_roles: :class:`bool` suppress_roles: :class:`bool`
Indicates if role mentions should be suppressed for the guild. 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. Indicates if push notifications should be sent to mobile devices for this guild.
hide_muted_channels: :class:`bool` hide_muted_channels: :class:`bool`
Indicates if channels that are muted should be hidden from the sidebar. 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 Raises
------- -------
@ -617,19 +728,24 @@ class GuildSettings:
""" """
payload = {} payload = {}
if muted is not MISSING: if muted_until is not MISSING:
payload['muted'] = muted if not muted_until:
payload['muted'] = False
if duration is not MISSING: else:
if muted is MISSING:
payload['muted'] = True payload['muted'] = True
if muted_until is True:
if duration is not None: payload['mute_config'] = {'selected_time_window': -1, 'end_time': None}
mute_config = { else:
'selected_time_window': duration * 3600, if muted_until.tzinfo is None:
'end_time': (datetime.utcnow() + timedelta(hours=duration)).isoformat(), raise TypeError(
} 'muted_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
payload['mute_config'] = mute_config )
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: if level is not MISSING:
payload['message_notifications'] = level.value payload['message_notifications'] = level.value
@ -640,12 +756,187 @@ class GuildSettings:
if suppress_roles is not MISSING: if suppress_roles is not MISSING:
payload['suppress_roles'] = suppress_roles payload['suppress_roles'] = suppress_roles
if mobile_push_notifications is not MISSING: if mobile_push is not MISSING:
payload['mobile_push'] = mobile_push_notifications payload['mobile_push'] = mobile_push
if hide_muted_channels is not MISSING: if hide_muted_channels is not MISSING:
payload['hide_muted_channels'] = hide_muted_channels 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) 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 .stage_instance import StageInstance
from .threads import Thread, ThreadMember from .threads import Thread, ThreadMember
from .sticker import GuildSticker from .sticker import GuildSticker
from .settings import UserSettings, GuildSettings from .settings import UserSettings, GuildSettings, ChannelSettings, TrackingSettings
from .tracking import Tracking
from .interactions import Interaction from .interactions import Interaction
from .permissions import Permissions, PermissionOverwrite from .permissions import Permissions, PermissionOverwrite
from .member import _ClientStatus from .member import _ClientStatus
@ -457,7 +456,8 @@ class ConnectionState:
self.user: Optional[ClientUser] = None self.user: Optional[ClientUser] = None
self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary() self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary()
self.settings: Optional[UserSettings] = None 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.connections: Dict[str, Connection] = {}
self.analytics_token: Optional[str] = None self.analytics_token: Optional[str] = None
self.preferred_regions: List[str] = [] self.preferred_regions: List[str] = []
@ -817,7 +817,6 @@ class ConnectionState:
self.clear() self.clear()
data = self._ready_data data = self._ready_data
guild_settings = data.get('user_guild_settings', {}).get('entries', [])
# Temp user parsing # Temp user parsing
temp_users: Dict[int, UserPayload] = {int(data['user']['id']): data['user']} temp_users: Dict[int, UserPayload] = {int(data['user']['id']): data['user']}
@ -833,11 +832,6 @@ class ConnectionState:
data.get('merged_members', []), data.get('merged_members', []),
extra_data['merged_presences'].get('guilds', []), 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: for presence in merged_presences:
presence['user'] = {'id': presence['user_id']} # type: ignore # :( presence['user'] = {'id': presence['user_id']} # type: ignore # :(
@ -892,7 +886,11 @@ class ConnectionState:
self.analytics_token = data.get('analytics_token') self.analytics_token = data.get('analytics_token')
self.preferred_regions = data.get('geo_ordered_rtc_regions', ['us-central']) self.preferred_regions = data.get('geo_ordered_rtc_regions', ['us-central'])
self.settings = UserSettings(data=data.get('user_settings', {}), state=self) 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.country_code = data.get('country_code', 'US')
self.session_type = data.get('session_type', 'normal') self.session_type = data.get('session_type', 'normal')
self.connections = {c['id']: Connection(state=self, data=c) for c in data.get('connected_accounts', [])} 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) self.dispatch('internal_settings_update', old_settings, new_settings)
def parse_user_guild_settings_update(self, data) -> None: def parse_user_guild_settings_update(self, data) -> None:
guild_id = int(data['guild_id']) guild_id = utils._get_as_snowflake(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
settings = guild.notification_settings settings = self.guild_settings.get(guild_id)
if settings is not None: if settings is not None:
old_settings = copy.copy(settings) old_settings = copy.copy(settings)
settings._update(data) settings._update(data)
@ -2316,3 +2310,9 @@ class ConnectionState:
def create_interaction_application(self, data: dict) -> InteractionApplication: def create_interaction_application(self, data: dict) -> InteractionApplication:
return InteractionApplication(state=self, data=data) 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 import json
from random import choice from random import choice
from typing import Dict, overload, Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING
from .utils import MISSING from .utils import MISSING
@ -37,12 +37,12 @@ if TYPE_CHECKING:
from .enums import ChannelType from .enums import ChannelType
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
from .state import ConnectionState
# fmt: off
__all__ = ( __all__ = (
'ContextProperties', 'ContextProperties',
'Tracking',
) )
# fmt: on
class ContextProperties: # Thank you Discord-S.C.U.M 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: def __hash__(self) -> int:
return hash(self.value) 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] restricted_guilds = [str(x.id) for x in restricted_guilds]
payload['restricted_guilds'] = 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) status = kwargs.pop('status', None)
if status: if status:
payload['status'] = status.value 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. 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 .. class:: ContentFilter
Specifies a :class:`Guild`\'s explicit content filter, which is the machine Specifies a :class:`Guild`\'s explicit content filter, which is the machine
@ -4303,9 +4321,14 @@ Settings
.. autoclass:: ChannelSettings() .. autoclass:: ChannelSettings()
:members: :members:
.. attributetable:: Tracking .. attributetable:: TrackingSettings
.. autoclass:: TrackingSettings()
:members:
.. attributetable:: EmailSettings
.. autoclass:: Tracking() .. autoclass:: EmailSettings()
:members: :members:
.. attributetable:: GuildFolder .. attributetable:: GuildFolder

Loading…
Cancel
Save