Browse Source

Implement guild notification settings

pull/10109/head
dolfies 4 years ago
parent
commit
1646421cc0
  1. 9
      discord/abc.py
  2. 17
      discord/enums.py
  3. 13
      discord/guild.py
  4. 2
      discord/http.py
  5. 269
      discord/settings.py
  6. 37
      discord/state.py
  7. 18
      discord/tracking.py
  8. 11
      discord/user.py

9
discord/abc.py

@ -54,6 +54,7 @@ from .invite import Invite
from .file import File from .file import File
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem from .sticker import GuildSticker, StickerItem
from .settings import ChannelSettings
from . import utils from . import utils
__all__ = ( __all__ = (
@ -82,7 +83,6 @@ if TYPE_CHECKING:
from .channel import DMChannel, GroupChannel, PartialMessageable, PrivateChannel, TextChannel, VocalGuildChannel from .channel import DMChannel, GroupChannel, PartialMessageable, PrivateChannel, TextChannel, VocalGuildChannel
from .threads import Thread from .threads import Thread
from .enums import InviteTarget from .enums import InviteTarget
from .ui.view import View
from .types.channel import ( from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload, PermissionOverwrite as PermissionOverwritePayload,
Channel as ChannelPayload, Channel as ChannelPayload,
@ -414,6 +414,13 @@ class GuildChannel:
if tmp: if tmp:
tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index]
@property
def notification_settings(self) -> ChannelSettings:
""":class:`ChannelSettings`: Returns the notification settings for this channel"""
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
@property @property
def changed_roles(self) -> List[Role]: def changed_roles(self) -> List[Role]:
"""List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from """List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from

17
discord/enums.py

@ -367,7 +367,7 @@ class DefaultAvatar(Enum):
return self.name return self.name
class RelationshipType(Enum): class RelationshipType(Enum, comparable=True):
friend = 1 friend = 1
blocked = 2 blocked = 2
incoming_request = 3 incoming_request = 3
@ -376,8 +376,15 @@ class RelationshipType(Enum):
class NotificationLevel(Enum, comparable=True): class NotificationLevel(Enum, comparable=True):
all_messages = 0 all_messages = 0
all = 0
only_mentions = 1 only_mentions = 1
nothing = 2
none = 2
server_default = 3
default = 3
def __int__(self):
return self.value
class AuditLogActionCategory(Enum): class AuditLogActionCategory(Enum):
create = 1 create = 1
@ -557,7 +564,7 @@ class HypeSquadHouse(Enum):
balance = 3 balance = 3
class PremiumType(Enum): class PremiumType(Enum, comparable=True):
nitro_classic = 1 nitro_classic = 1
nitro = 2 nitro = 2
@ -614,7 +621,7 @@ class ReportType(Enum):
return self.value return self.value
class RelationshipAction(Enum): class RelationshipAction(Enum, comparable=True):
send_friend_request = 'request' send_friend_request = 'request'
unfriend = 'unfriend' unfriend = 'unfriend'
accept_request = 'accept' accept_request = 'accept'
@ -624,12 +631,12 @@ class RelationshipAction(Enum):
remove_pending_request = 'remove' remove_pending_request = 'remove'
class UnavailableGuildType(Enum): class UnavailableGuildType(Enum, comparable=True):
existing = 'ready' existing = 'ready'
joined = 'joined' joined = 'joined'
class RequiredActionType(Enum): class RequiredActionType(Enum, comparable=True):
verify_phone = 'REQUIRE_VERIFIED_PHONE' verify_phone = 'REQUIRE_VERIFIED_PHONE'
verify_email = 'REQUIRE_VERIFIED_EMAIL' verify_email = 'REQUIRE_VERIFIED_EMAIL'
captcha = 'REQUIRE_CAPTCHA' captcha = 'REQUIRE_CAPTCHA'

13
discord/guild.py

@ -77,6 +77,7 @@ from .stage_instance import StageInstance
from .threads import Thread, ThreadMember from .threads import Thread, ThreadMember
from .sticker import GuildSticker from .sticker import GuildSticker
from .file import File from .file import File
from .settings import GuildSettings
__all__ = ( __all__ = (
@ -237,6 +238,10 @@ class Guild(Hashable):
premium_progress_bar_enabled: :class:`bool` premium_progress_bar_enabled: :class:`bool`
Whether the guild has the premium progress bar enabled. Whether the guild has the premium progress bar enabled.
.. versionadded:: 2.0
notification_settings: :class:`GuildSettings`
The notification settings for the guild.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
@ -265,6 +270,7 @@ class Guild(Hashable):
'owner_application_id', 'owner_application_id',
'vanity_code', 'vanity_code',
'premium_progress_bar_enabled', 'premium_progress_bar_enabled',
'notification_settings',
'_members', '_members',
'_channels', '_channels',
'_icon', '_icon',
@ -304,6 +310,7 @@ class Guild(Hashable):
self._threads: Dict[int, Thread] = {} self._threads: Dict[int, Thread] = {}
self._stage_instances: Dict[int, StageInstance] = {} self._stage_instances: Dict[int, StageInstance] = {}
self._state: ConnectionState = state self._state: ConnectionState = state
self.notification_settings: Optional[GuildSettings] = None
self._from_data(data) self._from_data(data)
# Get it running # Get it running
@ -476,6 +483,9 @@ class Guild(Hashable):
large = None if member_count is None else member_count >= 250 large = None if member_count is None else 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)
for mdata in guild.get('merged_members', []): for mdata in guild.get('merged_members', []):
try: try:
member = Member(data=mdata, guild=self, state=state) member = Member(data=mdata, guild=self, state=state)
@ -490,9 +500,6 @@ class Guild(Hashable):
if member is not None: if member is not None:
member._presence_update(presence, empty_tuple) member._presence_update(presence, empty_tuple)
def _update_settings(self, data) -> None:
pass # TODO
@property @property
def channels(self) -> List[GuildChannel]: def channels(self) -> List[GuildChannel]:
"""List[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" """List[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild."""

2
discord/http.py

@ -1194,7 +1194,7 @@ class HTTPClient:
return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason) return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason)
def edit_guild_settings(self, guild_id: Snowflake, **fields): # TODO: type and add more than just muting def edit_guild_settings(self, guild_id: Snowflake, fields): # TODO: type
return self.request(Route('PATCH', '/users/@me/guilds/{guild_id}/settings', guild_id=guild_id), json=fields) return self.request(Route('PATCH', '/users/@me/guilds/{guild_id}/settings', guild_id=guild_id), json=fields)
def get_template(self, code: str) -> Response[template.Template]: def get_template(self, code: str) -> Response[template.Template]:

269
discord/settings.py

@ -24,16 +24,24 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, TYPE_CHECKING from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
from .enums import FriendFlags, StickerAnimationOptions, Theme, UserContentFilter, try_enum from .enums import FriendFlags, NotificationLevel, StickerAnimationOptions, Theme, UserContentFilter, try_enum
from .guild_folder import GuildFolder from .guild_folder import GuildFolder
from .utils import MISSING, parse_time, utcnow
if TYPE_CHECKING: if TYPE_CHECKING:
from .abc import GuildChannel
from .guild import Guild from .guild import Guild
from .state import ConnectionState from .state import ConnectionState
from .tracking import Tracking
__all__ = ('UserSettings',) __all__ = (
'ChannelSettings',
'GuildSettings',
'UserSettings',
)
class UserSettings: class UserSettings:
@ -161,6 +169,11 @@ class UserSettings:
else: else:
setattr(self, '_' + key, value) setattr(self, '_' + key, value)
@property
def tracking(self) -> Optional[Tracking]:
"""Returns your tracking settings if available."""
return self._state.consents
@property @property
def animate_stickers(self) -> StickerAnimationOptions: def animate_stickers(self) -> StickerAnimationOptions:
"""Whether or not to animate stickers in the chat.""" """Whether or not to animate stickers in the chat."""
@ -201,3 +214,253 @@ class UserSettings:
def _get_guild(self, id: int) -> Optional[Guild]: def _get_guild(self, id: int) -> Optional[Guild]:
return self._state._get_guild(int(id)) return self._state._get_guild(int(id))
class MuteConfig:
def __init__(self, muted: bool, config: Dict[str, Union[str, int]]) -> None:
until = parse_time(config.get('end_time'))
if until is not None:
if until <= utcnow():
muted = False
until = None
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 __bool__(self) -> bool:
return bool(self.muted)
def __eq__(self, other) -> bool:
return self.muted == other
def __ne__(self, other) -> bool:
return not self.muted == other
class ChannelSettings:
"""Represents a channel's notification settings"""
if TYPE_CHECKING:
_channel_id: int
level: NotificationLevel
muted: MuteConfig
collapsed: bool
def __init__(self, guild_id, *, data: Dict[str, any] = {}, state: ConnectionState) -> None:
self._guild_id: int = guild_id
self._state = state
self._update(data)
def _update(self, data: Dict[str, Any]) -> None:
breakpoint()
self._channel_id = int(data['channel_id'])
self.collapsed = data.get('collapsed', False)
self.level = try_enum(NotificationLevel, data.get('message_notifications', 3)) # type: ignore
self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {})
@property
def channel(self) -> Optional[GuildChannel]:
"""Optional[:class:`GuildChannel]: Returns the channel these settings are for."""
guild = self._state._get_guild(self._guild_id)
return guild and guild.get_channel(self._channel_id)
async def edit(self,
*,
muted: bool = MISSING,
duration: Optional[int] = MISSING,
collapsed: bool = MISSING,
level: NotificationLevel = MISSING,
) -> Optional[ChannelSettings]:
"""|coro|
Edits the channel's notification settings.
All parameters are optional.
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.
collapsed: :class:`bool`
Unknown.
level: :class:`NotificationLevel`
Determines what level of notifications you receive for the channel.
Raises
-------
HTTPException
Editing the settings failed.
Returns
--------
Optional[:class:`ChannelSettings`]
The new notification settings. This is only returned if something is updated.
"""
payload = {}
if muted is not MISSING:
payload['muted'] = muted
if duration is not MISSING:
if muted is MISSING:
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 collapsed is not MISSING:
payload['collapsed'] = collapsed
if level is not MISSING:
payload['message_notifications'] = level.value
if payload:
fields = {'channel_overrides': {str(self._channel_id): payload}}
data = self._state.http.edit_guild_settings(self._guild_id, fields)
if data:
return ChannelSettings(
self._guild_id,
data=data['channel_overrides'][str(self._channel_id)],
state=self._state
)
class GuildSettings:
"""Represents a guild's notification settings."""
if TYPE_CHECKING:
_channel_overrides: Dict[int, ChannelSettings]
_guild_id: int
version: int
muted: MuteConfig
suppress_everyone: bool
suppress_roles: bool
hide_muted_channels: bool
mobile_push_notifications: bool
level: NotificationLevel
def __init__(self, *, data: Dict[str, Any], state: ConnectionState) -> None:
self._state = state
self._update(data)
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
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.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
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) -> Optional[Guild]:
"""Optional[:class:`Guild`]: Returns the guild that these settings are for."""
return self._state._get_guild(self._guild_id)
@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: bool = MISSING,
duration: Optional[int] = MISSING,
level: NotificationLevel = MISSING,
suppress_everyone: bool = MISSING,
suppress_roles: bool = MISSING,
mobile_push_notifications: bool = MISSING,
hide_muted_channels: bool = MISSING,
) -> Optional[GuildSettings]:
"""|coro|
Edits the guild's notification settings.
All parameters are optional.
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.
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`
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.
Raises
-------
HTTPException
Editing the settings failed.
Returns
--------
Optional[:class:`GuildSettings`]
The new notification settings. This is only returned if something is updated.
"""
payload = {}
if muted is not MISSING:
payload['muted'] = muted
if duration is not MISSING:
if muted is MISSING:
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 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_notifications is not MISSING:
payload['mobile_push'] = mobile_push_notifications
if hide_muted_channels is not MISSING:
payload['hide_muted_channels'] = hide_muted_channels
if payload:
data = self._state.http.edit_guild_settings(self._guild_id, payload)
if data:
return GuildSettings(data=data, state=self._state)

37
discord/state.py

@ -58,6 +58,7 @@ 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 from .settings import UserSettings
from .tracking import Tracking
if TYPE_CHECKING: if TYPE_CHECKING:
@ -232,7 +233,11 @@ class ConnectionState:
def clear(self) -> None: def clear(self) -> None:
self.user: Optional[ClientUser] = None self.user: Optional[ClientUser] = None
self.settings: Optional[UserSettings] = None self.settings: Optional[UserSettings] = None
self.consents: Optional[Tracking] = None
self.analytics_token: Optional[str] = None self.analytics_token: Optional[str] = None
self.session_id: Optional[str] = None
self.connected_accounts: Optional[List[dict]] = None
self.preferred_region: Optional[VoiceRegion] = None
# Originally, this code used WeakValueDictionary to maintain references to the # Originally, this code used WeakValueDictionary to maintain references to the
# global user mapping # global user mapping
@ -591,9 +596,8 @@ class ConnectionState:
self.clear() self.clear()
# Merge with READY data extra_data, data = data, self._ready_data
extra_data = data guild_settings = data.get('user_guild_settings', {}).get('entries', [])
data = self._ready_data
# Discord bad # Discord bad
for guild_data, guild_extra, merged_members, merged_me, merged_presences in zip( for guild_data, guild_extra, merged_members, merged_me, merged_presences in zip(
@ -603,6 +607,11 @@ 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(
lambda i: i['guild_id'] == guild_data['id'],
guild_settings,
) or {'guild_id': guild_data['id']}
guild_data['voice_states'] = guild_extra.get('voice_states', []) guild_data['voice_states'] = guild_extra.get('voice_states', [])
guild_data['merged_members'] = merged_me guild_data['merged_members'] = merged_me
guild_data['merged_members'].extend(merged_members) guild_data['merged_members'].extend(merged_members)
@ -646,9 +655,12 @@ class ConnectionState:
self._add_private_channel(factory(me=user, data=pm, state=self)) self._add_private_channel(factory(me=user, data=pm, state=self))
# Extras # Extras
self.session_id = data.get('session_id')
self.analytics_token = data.get('analytics_token')
region = data.get('geo_ordered_rtc_regions', ['us-west'])[0] region = data.get('geo_ordered_rtc_regions', ['us-west'])[0]
self.preferred_region = try_enum(VoiceRegion, region) self.preferred_region = try_enum(VoiceRegion, region)
self.settings = UserSettings(data=data.get('user_settings', {}), state=self) self.settings = settings = UserSettings(data=data.get('user_settings', {}), state=self)
self.consents = Tracking(data.get('consents', {}))
# We're done # We're done
del self._ready_data del self._ready_data
@ -656,7 +668,7 @@ class ConnectionState:
self.dispatch('connect') self.dispatch('connect')
self._ready_task = asyncio.create_task(self._delay_ready()) self._ready_task = asyncio.create_task(self._delay_ready())
def parse_resumed(self, data) -> None: def parse_resumed(self, _) -> None:
self.dispatch('resumed') self.dispatch('resumed')
def parse_message_create(self, data) -> None: def parse_message_create(self, data) -> None:
@ -818,6 +830,19 @@ class ConnectionState:
if ref: if ref:
ref._update(data) ref._update(data)
def parse_user_settings_update(self, data) -> None:
new_settings = self.settings
old_settings = copy.copy(new_settings)
new_settings._update(data)
self.dispatch('settings_update', old_settings, new_settings)
def parse_user_guild_settings_update(self, data) -> None:
guild = self.get_guild(int(data['guild_id']))
new_settings = guild.notification_settings
old_settings = copy.copy(new_settings)
new_settings._update(data)
self.dispatch('guild_settings_update', old_settings, new_settings)
def parse_invite_create(self, data) -> None: def parse_invite_create(self, data) -> None:
invite = Invite.from_gateway(state=self, data=data) invite = Invite.from_gateway(state=self, data=data)
self.dispatch('invite_create', invite) self.dispatch('invite_create', invite)
@ -841,7 +866,7 @@ class ConnectionState:
if channel_type is ChannelType.group: if channel_type is ChannelType.group:
channel = self._get_private_channel(channel_id) channel = self._get_private_channel(channel_id)
old_channel = copy.copy(channel) old_channel = copy.copy(channel)
# the channel is a GroupChannel # The channel is a GroupChannel
channel._update_group(data) # type: ignore channel._update_group(data) # type: ignore
self.dispatch('private_channel_update', old_channel, channel) self.dispatch('private_channel_update', old_channel, channel)
return return

18
discord/tracking.py

@ -31,7 +31,10 @@ from typing import Any, Dict, Optional
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
__all__ = ('ContextProperties',) __all__ = (
'ContextProperties',
'Tracking',
)
class ContextProperties: # Thank you Discord-S.C.U.M class ContextProperties: # Thank you Discord-S.C.U.M
@ -250,3 +253,16 @@ class ContextProperties: # Thank you Discord-S.C.U.M
def __ne__(self, other) -> bool: def __ne__(self, other) -> bool:
return not self.__eq__(other) return not self.__eq__(other)
class Tracking:
"""Represents your Discord tracking settings.
Attributes
----------
personalization: :class:`bool`
Whether you have consented to your data being used for personalization.
"""
def __init__(self, data: Dict[str, Any]): # TODO: rest of the values
self.personalization = data.get('personalization', {}).get('consented', False)

11
discord/user.py

@ -644,6 +644,11 @@ class ClientUser(BaseUser):
self.premium_type = try_enum(PremiumType, data.get('premium_type', None)) self.premium_type = try_enum(PremiumType, data.get('premium_type', None))
self.bio = data.get('bio') or None self.bio = data.get('bio') or None
@property
def connected_accounts(self) -> Optional[List[dict]]:
"""Optional[List[:class:`dict`]]: Returns a list of all linked accounts for this user if available."""
return self._state._connected_accounts
def get_relationship(self, user_id: int) -> Relationship: def get_relationship(self, user_id: int) -> Relationship:
"""Retrieves the :class:`Relationship` if applicable. """Retrieves the :class:`Relationship` if applicable.
@ -848,6 +853,9 @@ class ClientUser(BaseUser):
Edits the client user's settings. Edits the client user's settings.
.. versionchanged:: 2.0
The edit is no longer in-place, instead the newly edited settings are returned.
Parameters Parameters
---------- ----------
afk_timeout: :class:`int` afk_timeout: :class:`int`
@ -960,8 +968,7 @@ class ClientUser(BaseUser):
state = self._state state = self._state
data = await state.http.edit_settings(**payload) data = await state.http.edit_settings(**payload)
state.settings = settings = UserSettings(data=data, state=self._state) return UserSettings(data=data, state=self._state)
return settings
class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):

Loading…
Cancel
Save