Browse Source

Implement discord.MessageFlags

Refactor flags placement and use it for suppression.
pull/2484/head
NCPlayz 5 years ago
committed by Rapptz
parent
commit
9c6a121644
  1. 3
      discord/__init__.py
  2. 237
      discord/flags.py
  3. 115
      discord/guild.py
  4. 10
      discord/http.py
  5. 38
      discord/message.py
  6. 6
      docs/api.rst

3
discord/__init__.py

@ -27,7 +27,8 @@ from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .activity import * from .activity import *
from .channel import * from .channel import *
from .guild import Guild, SystemChannelFlags from .guild import Guild
from .flags import SystemChannelFlags, MessageFlags
from .relationship import Relationship from .relationship import Relationship
from .member import Member, VoiceState from .member import Member, VoiceState
from .message import Message, Attachment from .message import Message, Attachment

237
discord/flags.py

@ -0,0 +1,237 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
__all__ = (
'SystemChannelFlags',
'MessageFlags',
)
class _flag_descriptor:
def __init__(self, func):
self.flag = func(None)
self.__doc__ = func.__doc__
def __get__(self, instance, owner):
return instance._has_flag(self.flag)
def __set__(self, instance, value):
instance._set_flag(self.flag, value)
def fill_with_flags(cls):
cls.VALID_FLAGS = {
name: value.flag
for name, value in cls.__dict__.items()
if isinstance(value, _flag_descriptor)
}
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
cls.ALL_OFF_VALUE = -1 + (2 ** max_bits)
return cls
@fill_with_flags
class SystemChannelFlags:
r"""Wraps up a Discord system channel flag value.
Similar to :class:`Permissions`\, the properties provided are two way.
You can set and retrieve individual bits using the properties as if they
were regular bools. This allows you to edit the system flags easily.
To construct an object you can pass keyword arguments denoting the flags
to enable or disable.
.. container:: operations
.. describe:: x == y
Checks if two flags are equal.
.. describe:: x != y
Checks if two flags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ('value',)
def __init__(self, **kwargs):
self.value = self.ALL_OFF_VALUE
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key)
setattr(self, key, value)
@classmethod
def _from_value(cls, value):
self = cls.__new__(cls)
self.value = value
return self
def __eq__(self, other):
return isinstance(other, SystemChannelFlags) and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.value)
def __repr__(self):
return '<SystemChannelFlags value=%s>' % self.value
def __iter__(self):
for name, value in self.__class__.__dict__.items():
if isinstance(value, _flag_descriptor):
yield (name, self._has_flag(value.flag))
# For some reason the flags for system channels are "inverted"
# ergo, if they're set then it means "suppress" (off in the GUI toggle)
# Since this is counter-intuitive from an API perspective and annoying
# these will be inverted automatically
def _has_flag(self, o):
return (self.value & o) != o
def _set_flag(self, o, toggle):
if toggle is True:
self.value &= ~o
elif toggle is False:
self.value |= o
else:
raise TypeError('Value to set for SystemChannelFlags must be a bool.')
@_flag_descriptor
def join_notifications(self):
""":class:`bool`: Returns ``True`` if the system channel is used for member join notifications."""
return 1
@_flag_descriptor
def premium_subscriptions(self):
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
return 2
@fill_with_flags
class MessageFlags:
r"""Wraps up a Discord Message flag value.
See :class:`SystemChannelFlags`.
.. container:: operations
.. describe:: x == y
Checks if two flags are equal.
.. describe:: x != y
Checks if two flags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ('value',)
def __init__(self, **kwargs):
self.value = 0
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key)
setattr(self, key, value)
@classmethod
def _from_value(cls, value):
self = cls.__new__(cls)
self.value = value
return self
def __eq__(self, other):
return isinstance(other, MessageFlags) and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.value)
def __repr__(self):
return '<MessageFlags value=%s>' % self.value
def __iter__(self):
for name, value in self.__class__.__dict__.items():
if isinstance(value, _flag_descriptor):
yield (name, self._has_flag(value.flag))
def _has_flag(self, o):
return (self.value & o) == o
def _set_flag(self, o, toggle):
if toggle is True:
self.value |= o
elif toggle is False:
self.value &= o
else:
raise TypeError('Value to set for MessageFlags must be a bool.')
@_flag_descriptor
def crossposted(self):
""":class:`bool`: Returns ``True`` if the message is the original crossposted message."""
return 1
@_flag_descriptor
def is_crossposted(self):
""":class:`bool`: Returns ``True`` if the message was crossposted from another channel."""
return 2
@_flag_descriptor
def suppress_embeds(self):
""":class:`bool`: Returns ``True`` if the message's embeds have been suppressed."""
return 4
@_flag_descriptor
def source_message_deleted(self):
""":class:`bool`: Returns ``True`` if the source message for this crosspost has been deleted."""
return 8

115
discord/guild.py

@ -45,124 +45,11 @@ from .iterators import AuditLogIterator, MemberIterator
from .webhook import Webhook from .webhook import Webhook
from .widget import Widget from .widget import Widget
from .asset import Asset from .asset import Asset
from .flags import SystemChannelFlags
BanEntry = namedtuple('BanEntry', 'reason user') BanEntry = namedtuple('BanEntry', 'reason user')
_GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize') _GuildLimit = namedtuple('_GuildLimit', 'emoji bitrate filesize')
class _flag_descriptor:
def __init__(self, func):
self.flag = func(None)
self.__doc__ = func.__doc__
def __get__(self, instance, owner):
return instance._has_flag(self.flag)
def __set__(self, instance, value):
instance._set_flag(self.flag, value)
def fill_with_flags(cls):
cls.VALID_FLAGS = {
name: value.flag
for name, value in cls.__dict__.items()
if isinstance(value, _flag_descriptor)
}
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
cls.ALL_OFF_VALUE = -1 + (2 ** max_bits)
return cls
@fill_with_flags
class SystemChannelFlags:
r"""Wraps up a Discord system channel flag value.
Similar to :class:`Permissions`\, the properties provided are two way.
You can set and retrieve individual bits using the properties as if they
were regular bools. This allows you to edit the system flags easily.
To construct an object you can pass keyword arguments denoting the flags
to enable or disable.
.. container:: operations
.. describe:: x == y
Checks if two flags are equal.
.. describe:: x != y
Checks if two flags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ('value',)
def __init__(self, **kwargs):
self.value = self.ALL_OFF_VALUE
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key)
setattr(self, key, value)
@classmethod
def _from_value(cls, value):
self = cls.__new__(cls)
self.value = value
return self
def __eq__(self, other):
return isinstance(other, SystemChannelFlags) and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.value)
def __repr__(self):
return '<SystemChannelFlags value=%s>' % self.value
def __iter__(self):
for name, value in self.__class__.__dict__.items():
if isinstance(value, _flag_descriptor):
yield (name, self._has_flag(value.flag))
# For some reason the flags in the Discord API are "inverted"
# ergo, if they're set then it means "suppress" (off in the GUI toggle)
# Since this is counter-intuitive from an API perspective and annoying
# these will be inverted automatically
def _has_flag(self, o):
return (self.value & o) != o
def _set_flag(self, o, toggle):
if toggle is True:
self.value &= ~o
elif toggle is False:
self.value |= o
else:
raise TypeError('Value to set for SystemChannelFlags must be a bool.')
@_flag_descriptor
def join_notifications(self):
""":class:`bool`: Returns True if the system channel is used for member join notifications."""
return 1
@_flag_descriptor
def premium_subscriptions(self):
""":class:`bool`: Returns True if the system channel is used for Nitro boosting notifications."""
return 2
class Guild(Hashable): class Guild(Hashable):
"""Represents a Discord guild. """Represents a Discord guild.

10
discord/http.py

@ -372,12 +372,6 @@ class HTTPClient:
r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id) r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id)
return self.request(r, json=fields) return self.request(r, json=fields)
def suppress_message_embeds(self, channel_id, message_id, *, suppress):
payload = { 'suppress': suppress }
r = Route('POST', '/channels/{channel_id}/messages/{message_id}/suppress-embeds',
channel_id=channel_id, message_id=message_id)
return self.request(r, json=payload)
def add_reaction(self, channel_id, message_id, emoji): def add_reaction(self, channel_id, message_id, emoji):
r = Route('PUT', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', r = Route('PUT', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me',
channel_id=channel_id, message_id=message_id, emoji=emoji) channel_id=channel_id, message_id=message_id, emoji=emoji)
@ -430,6 +424,10 @@ class HTTPClient:
return self.request(Route('GET', '/channels/{channel_id}/messages', channel_id=channel_id), params=params) return self.request(Route('GET', '/channels/{channel_id}/messages', channel_id=channel_id), params=params)
def publish_message(self, channel_id, message_id):
return self.request(Route('POST', '/channels/{channel_id}/messages/{message_id}/crosspost',
channel_id=channel_id, message_id=message_id))
def pin_message(self, channel_id, message_id): def pin_message(self, channel_id, message_id):
return self.request(Route('PUT', '/channels/{channel_id}/pins/{message_id}', return self.request(Route('PUT', '/channels/{channel_id}/pins/{message_id}',
channel_id=channel_id, message_id=message_id)) channel_id=channel_id, message_id=message_id))

38
discord/message.py

@ -38,6 +38,7 @@ from .enums import MessageType, try_enum
from .errors import InvalidArgument, ClientException, HTTPException from .errors import InvalidArgument, ClientException, HTTPException
from .embeds import Embed from .embeds import Embed
from .member import Member from .member import Member
from .flags import MessageFlags
class Attachment: class Attachment:
"""Represents an attachment from Discord. """Represents an attachment from Discord.
@ -237,6 +238,8 @@ class Message:
A list of attachments given to a message. A list of attachments given to a message.
pinned: :class:`bool` pinned: :class:`bool`
Specifies if the message is currently pinned. Specifies if the message is currently pinned.
flags: :class:`MessageFlags`
Extra features of the message.
reactions : List[:class:`Reaction`] reactions : List[:class:`Reaction`]
Reactions to a message. Reactions can be either custom emoji or standard unicode emoji. Reactions to a message. Reactions can be either custom emoji or standard unicode emoji.
activity: Optional[:class:`dict`] activity: Optional[:class:`dict`]
@ -263,7 +266,7 @@ class Message:
'mention_everyone', 'embeds', 'id', 'mentions', 'author', 'mention_everyone', 'embeds', 'id', 'mentions', 'author',
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments', '_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned', '_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags',
'_cs_system_content', '_cs_guild', '_state', 'reactions', '_cs_system_content', '_cs_guild', '_state', 'reactions',
'application', 'activity') 'application', 'activity')
@ -280,19 +283,20 @@ class Message:
self._edited_timestamp = utils.parse_time(data['edited_timestamp']) self._edited_timestamp = utils.parse_time(data['edited_timestamp'])
self.type = try_enum(MessageType, data['type']) self.type = try_enum(MessageType, data['type'])
self.pinned = data['pinned'] self.pinned = data['pinned']
self.flags = MessageFlags._from_value(data.get('flags', 0))
self.mention_everyone = data['mention_everyone'] self.mention_everyone = data['mention_everyone']
self.tts = data['tts'] self.tts = data['tts']
self.content = data['content'] self.content = data['content']
self.nonce = data.get('nonce') self.nonce = data.get('nonce')
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call'): for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
try: try:
getattr(self, '_handle_%s' % handler)(data[handler]) getattr(self, '_handle_%s' % handler)(data[handler])
except KeyError: except KeyError:
continue continue
def __repr__(self): def __repr__(self):
return '<Message id={0.id} channel={0.channel!r} type={0.type!r} author={0.author!r}>'.format(self) return '<Message id={0.id} channel={0.channel!r} type={0.type!r} author={0.author!r} flags={0.flags!r}>'.format(self)
def _try_patch(self, data, key, transform=None): def _try_patch(self, data, key, transform=None):
try: try:
@ -361,6 +365,9 @@ class Message:
def _handle_pinned(self, value): def _handle_pinned(self, value):
self.pinned = value self.pinned = value
def _handle_flags(self, value):
self.flags = MessageFlags._from_value(value)
def _handle_application(self, value): def _handle_application(self, value):
self.application = value self.application = value
@ -772,7 +779,9 @@ class Message:
except KeyError: except KeyError:
pass pass
else: else:
await self._state.http.suppress_message_embeds(self.channel.id, self.id, suppress=suppress) flags = MessageFlags._from_value(self.flags.value)
flags.suppress_embeds = suppress
fields['flags'] = flags.value
delete_after = fields.pop('delete_after', None) delete_after = fields.pop('delete_after', None)
@ -783,6 +792,27 @@ class Message:
if delete_after is not None: if delete_after is not None:
await self.delete(delay=delete_after) await self.delete(delay=delete_after)
async def publish(self):
"""|coro|
Publishes this message to your announcement channel.
You must have the :attr:`~Permissions.manage_messages` permission to use this.
.. note::
This can only be used by non-bot accounts.
Raises
-------
Forbidden
You do not have the proper permissions to publish this message.
HTTPException
Publishing the message failed.
"""
await self._state.http.publish_message(self.channel.id, self.id)
async def pin(self): async def pin(self):
"""|coro| """|coro|

6
docs/api.rst

@ -2507,6 +2507,12 @@ SystemChannelFlags
.. autoclass:: SystemChannelFlags .. autoclass:: SystemChannelFlags
:members: :members:
MessageFlags
~~~~~~~~~~~~
.. autoclass:: MessageFlags
:members:
Exceptions Exceptions
------------ ------------

Loading…
Cancel
Save