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 .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem
from .settings import ChannelSettings
from . import utils
__all__ = (
@ -82,7 +83,6 @@ if TYPE_CHECKING:
from .channel import DMChannel, GroupChannel, PartialMessageable, PrivateChannel, TextChannel, VocalGuildChannel
from .threads import Thread
from .enums import InviteTarget
from .ui.view import View
from .types.channel import (
PermissionOverwrite as PermissionOverwritePayload,
Channel as ChannelPayload,
@ -414,6 +414,13 @@ class GuildChannel:
if tmp:
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
def changed_roles(self) -> List[Role]:
"""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
class RelationshipType(Enum):
class RelationshipType(Enum, comparable=True):
friend = 1
blocked = 2
incoming_request = 3
@ -376,8 +376,15 @@ class RelationshipType(Enum):
class NotificationLevel(Enum, comparable=True):
all_messages = 0
all = 0
only_mentions = 1
nothing = 2
none = 2
server_default = 3
default = 3
def __int__(self):
return self.value
class AuditLogActionCategory(Enum):
create = 1
@ -557,7 +564,7 @@ class HypeSquadHouse(Enum):
balance = 3
class PremiumType(Enum):
class PremiumType(Enum, comparable=True):
nitro_classic = 1
nitro = 2
@ -614,7 +621,7 @@ class ReportType(Enum):
return self.value
class RelationshipAction(Enum):
class RelationshipAction(Enum, comparable=True):
send_friend_request = 'request'
unfriend = 'unfriend'
accept_request = 'accept'
@ -624,12 +631,12 @@ class RelationshipAction(Enum):
remove_pending_request = 'remove'
class UnavailableGuildType(Enum):
class UnavailableGuildType(Enum, comparable=True):
existing = 'ready'
joined = 'joined'
class RequiredActionType(Enum):
class RequiredActionType(Enum, comparable=True):
verify_phone = 'REQUIRE_VERIFIED_PHONE'
verify_email = 'REQUIRE_VERIFIED_EMAIL'
captcha = 'REQUIRE_CAPTCHA'

13
discord/guild.py

@ -77,6 +77,7 @@ from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
from .sticker import GuildSticker
from .file import File
from .settings import GuildSettings
__all__ = (
@ -237,6 +238,10 @@ class Guild(Hashable):
premium_progress_bar_enabled: :class:`bool`
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
"""
@ -265,6 +270,7 @@ class Guild(Hashable):
'owner_application_id',
'vanity_code',
'premium_progress_bar_enabled',
'notification_settings',
'_members',
'_channels',
'_icon',
@ -304,6 +310,7 @@ class Guild(Hashable):
self._threads: Dict[int, Thread] = {}
self._stage_instances: Dict[int, StageInstance] = {}
self._state: ConnectionState = state
self.notification_settings: Optional[GuildSettings] = None
self._from_data(data)
# Get it running
@ -476,6 +483,9 @@ class Guild(Hashable):
large = None if member_count is None else 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)
for mdata in guild.get('merged_members', []):
try:
member = Member(data=mdata, guild=self, state=state)
@ -490,9 +500,6 @@ class Guild(Hashable):
if member is not None:
member._presence_update(presence, empty_tuple)
def _update_settings(self, data) -> None:
pass # TODO
@property
def channels(self) -> List[GuildChannel]:
"""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)
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)
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 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 .utils import MISSING, parse_time, utcnow
if TYPE_CHECKING:
from .abc import GuildChannel
from .guild import Guild
from .state import ConnectionState
from .tracking import Tracking
__all__ = ('UserSettings',)
__all__ = (
'ChannelSettings',
'GuildSettings',
'UserSettings',
)
class UserSettings:
@ -161,6 +169,11 @@ class UserSettings:
else:
setattr(self, '_' + key, value)
@property
def tracking(self) -> Optional[Tracking]:
"""Returns your tracking settings if available."""
return self._state.consents
@property
def animate_stickers(self) -> StickerAnimationOptions:
"""Whether or not to animate stickers in the chat."""
@ -201,3 +214,253 @@ class UserSettings:
def _get_guild(self, id: int) -> Optional[Guild]:
return self._state._get_guild(int(id))
class MuteConfig:
def __init__(self, muted: bool, config: Dict[str, 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 .sticker import GuildSticker
from .settings import UserSettings
from .tracking import Tracking
if TYPE_CHECKING:
@ -232,7 +233,11 @@ class ConnectionState:
def clear(self) -> None:
self.user: Optional[ClientUser] = None
self.settings: Optional[UserSettings] = None
self.consents: Optional[Tracking] = 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
# global user mapping
@ -591,9 +596,8 @@ class ConnectionState:
self.clear()
# Merge with READY data
extra_data = data
data = self._ready_data
extra_data, data = data, self._ready_data
guild_settings = data.get('user_guild_settings', {}).get('entries', [])
# Discord bad
for guild_data, guild_extra, merged_members, merged_me, merged_presences in zip(
@ -603,6 +607,11 @@ class ConnectionState:
data.get('merged_members', []),
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['merged_members'] = merged_me
guild_data['merged_members'].extend(merged_members)
@ -646,9 +655,12 @@ class ConnectionState:
self._add_private_channel(factory(me=user, data=pm, state=self))
# 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]
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
del self._ready_data
@ -656,7 +668,7 @@ class ConnectionState:
self.dispatch('connect')
self._ready_task = asyncio.create_task(self._delay_ready())
def parse_resumed(self, data) -> None:
def parse_resumed(self, _) -> None:
self.dispatch('resumed')
def parse_message_create(self, data) -> None:
@ -818,6 +830,19 @@ class ConnectionState:
if ref:
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:
invite = Invite.from_gateway(state=self, data=data)
self.dispatch('invite_create', invite)
@ -841,7 +866,7 @@ class ConnectionState:
if channel_type is ChannelType.group:
channel = self._get_private_channel(channel_id)
old_channel = copy.copy(channel)
# the channel is a GroupChannel
# The channel is a GroupChannel
channel._update_group(data) # type: ignore
self.dispatch('private_channel_update', old_channel, channel)
return

18
discord/tracking.py

@ -31,7 +31,10 @@ from typing import Any, Dict, Optional
from .types.snowflake import Snowflake
__all__ = ('ContextProperties',)
__all__ = (
'ContextProperties',
'Tracking',
)
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:
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.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:
"""Retrieves the :class:`Relationship` if applicable.
@ -848,6 +853,9 @@ class ClientUser(BaseUser):
Edits the client user's settings.
.. versionchanged:: 2.0
The edit is no longer in-place, instead the newly edited settings are returned.
Parameters
----------
afk_timeout: :class:`int`
@ -960,8 +968,7 @@ class ClientUser(BaseUser):
state = self._state
data = await state.http.edit_settings(**payload)
state.settings = settings = UserSettings(data=data, state=self._state)
return settings
return UserSettings(data=data, state=self._state)
class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):

Loading…
Cancel
Save