Browse Source

Implement "partial" message events.

These are events that get triggered regardless of the state of the
message cache. Useful for getting data from before the bot was booted.
pull/609/merge
Rapptz 8 years ago
parent
commit
d239cc2666
  1. 2
      discord/__init__.py
  2. 50
      discord/emoji.py
  3. 10
      discord/message.py
  4. 2
      discord/reaction.py
  5. 65
      discord/state.py
  6. 4
      discord/utils.py
  7. 71
      docs/api.rst

2
discord/__init__.py

@ -20,7 +20,7 @@ __version__ = '1.0.0a'
from .client import Client, AppInfo from .client import Client, AppInfo
from .user import User, ClientUser, Profile from .user import User, ClientUser, Profile
from .game import Game from .game import Game
from .emoji import Emoji, PartialEmoji from .emoji import Emoji, PartialReactionEmoji
from .channel import * from .channel import *
from .guild import Guild from .guild import Guild
from .relationship import Relationship from .relationship import Relationship

50
discord/emoji.py

@ -30,7 +30,55 @@ from collections import namedtuple
from . import utils from . import utils
from .mixins import Hashable from .mixins import Hashable
PartialEmoji = namedtuple('PartialEmoji', 'id name') class PartialReactionEmoji(namedtuple('PartialReactionEmoji', 'name id')):
"""Represents a "partial" reaction emoji.
This model will be given in two scenarios:
- "Raw" data events such as :func:`on_raw_reaction_add`
- Custom emoji that the bot cannot see from e.g. :attr:`Message.reactions`
.. container:: operations
.. describe:: x == y
Checks if two emoji are the same.
.. describe:: x != y
Checks if two emoji are not the same.
.. describe:: hash(x)
Return the emoji's hash.
.. describe:: str(x)
Returns the emoji rendered for discord.
Attributes
-----------
name: str
The custom emoji name, if applicable, or the unicode codepoint
of the non-custom emoji.
id: Optional[int]
The ID of the custom emoji, if applicable.
"""
__slots__ = ()
def __str__(self):
if self.id is None:
return self.name
return '<:%s:%s>' % (self.name, self.id)
def is_custom_emoji(self):
"""Checks if this is a custom non-Unicode emoji."""
return self.id is not None
def is_unicode_emoji(self):
"""Checks if this is a Unicode emoji."""
return self.id is None
class Emoji(Hashable): class Emoji(Hashable):
"""Represents a custom emoji. """Represents a custom emoji.

10
discord/message.py

@ -198,10 +198,9 @@ class Message:
else: else:
setattr(self, key, transform(value)) setattr(self, key, transform(value))
def _add_reaction(self, data): def _add_reaction(self, data, emoji, user_id):
emoji = self._state.get_reaction_emoji(data['emoji'])
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
is_me = data['me'] = int(data['user_id']) == self._state.self_id is_me = data['me'] = user_id == self._state.self_id
if reaction is None: if reaction is None:
reaction = Reaction(message=self, data=data, emoji=emoji) reaction = Reaction(message=self, data=data, emoji=emoji)
@ -213,8 +212,7 @@ class Message:
return reaction return reaction
def _remove_reaction(self, data): def _remove_reaction(self, data, emoji, user_id):
emoji = self._state.get_reaction_emoji(data['emoji'])
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
if reaction is None: if reaction is None:
@ -225,7 +223,7 @@ class Message:
# sent bad data, or we stored improperly # sent bad data, or we stored improperly
reaction.count -= 1 reaction.count -= 1
if int(data['user_id']) == self._state.self_id: if user_id == self._state.self_id:
reaction.me = False reaction.me = False
if reaction.count == 0: if reaction.count == 0:
# this raises ValueError if something went wrong as well. # this raises ValueError if something went wrong as well.

2
discord/reaction.py

@ -67,7 +67,7 @@ class Reaction:
def __init__(self, *, message, data, emoji=None): def __init__(self, *, message, data, emoji=None):
self.message = message self.message = message
self.emoji = message._state.get_reaction_emoji(data['emoji']) if emoji is None else emoji self.emoji = emoji or message._state.get_reaction_emoji(data['emoji'])
self.count = data.get('count', 1) self.count = data.get('count', 1)
self.me = data.get('me') self.me = data.get('me')

65
discord/state.py

@ -26,7 +26,7 @@ DEALINGS IN THE SOFTWARE.
from .guild import Guild from .guild import Guild
from .user import User, ClientUser from .user import User, ClientUser
from .emoji import Emoji, PartialEmoji from .emoji import Emoji, PartialReactionEmoji
from .message import Message from .message import Message
from .relationship import Relationship from .relationship import Relationship
from .channel import * from .channel import *
@ -35,6 +35,7 @@ from .role import Role
from .enums import ChannelType, try_enum, Status from .enums import ChannelType, try_enum, Status
from .calls import GroupCall from .calls import GroupCall
from . import utils, compat from . import utils, compat
from .embeds import Embed
from collections import deque, namedtuple from collections import deque, namedtuple
import copy, enum, math import copy, enum, math
@ -314,20 +315,27 @@ class ConnectionState:
def parse_message_delete(self, data): def parse_message_delete(self, data):
message_id = int(data['id']) message_id = int(data['id'])
channel_id = int(data['channel_id'])
self.dispatch('raw_message_delete', message_id, channel_id)
found = self._get_message(message_id) found = self._get_message(message_id)
if found is not None: if found is not None:
self.dispatch('message_delete', found) self.dispatch('message_delete', found)
self._messages.remove(found) self._messages.remove(found)
def parse_message_delete_bulk(self, data): def parse_message_delete_bulk(self, data):
message_ids = set(map(int, data.get('ids', []))) message_ids = { int(x) for x in data.get('ids', []) }
to_be_deleted = list(filter(lambda m: m.id in message_ids, self._messages)) channel_id = int(data['channel_id'])
self.dispatch('raw_bulk_message_delete', message_ids, channel_id)
to_be_deleted = [message for message in self._messages if message.id in message_ids]
for msg in to_be_deleted: for msg in to_be_deleted:
self.dispatch('message_delete', msg) self.dispatch('message_delete', msg)
self._messages.remove(msg) self._messages.remove(msg)
def parse_message_update(self, data): def parse_message_update(self, data):
message = self._get_message(int(data['id'])) message_id = int(data['id'])
self.dispatch('raw_message_edit', message_id, data)
message = self._get_message(message_id)
if message is not None: if message is not None:
older_message = copy.copy(message) older_message = copy.copy(message)
if 'call' in data: if 'call' in data:
@ -335,36 +343,61 @@ class ConnectionState:
message._handle_call(data['call']) message._handle_call(data['call'])
elif 'content' not in data: elif 'content' not in data:
# embed only edit # embed only edit
message.embeds = data['embeds'] message.embeds = [Embed.from_data(d) for d in data['embeds']]
else: else:
message._update(channel=message.channel, data=data) message._update(channel=message.channel, data=data)
self.dispatch('message_edit', older_message, message) self.dispatch('message_edit', older_message, message)
def parse_message_reaction_add(self, data): def parse_message_reaction_add(self, data):
message = self._get_message(int(data['message_id'])) message_id = int(data['message_id'])
user_id = int(data['user_id'])
channel_id = int(data['channel_id'])
emoji_data = data['emoji']
emoji_id = utils._get_as_snowflake(emoji_data, 'id')
emoji = PartialReactionEmoji(id=emoji_id, name=emoji_data['name'])
self.dispatch('raw_reaction_add', emoji, message_id, channel_id, user_id)
# rich interface here
message = self._get_message(message_id)
if message is not None: if message is not None:
reaction = message._add_reaction(data) emoji = self._upgrade_partial_emoji(emoji)
user = self._get_reaction_user(message.channel, int(data['user_id'])) reaction = message._add_reaction(data, emoji, user_id)
user = self._get_reaction_user(message.channel, user_id)
if user: if user:
self.dispatch('reaction_add', reaction, user) self.dispatch('reaction_add', reaction, user)
def parse_message_reaction_remove_all(self, data): def parse_message_reaction_remove_all(self, data):
message = self._get_message(int(data['message_id'])) message_id = int(data['message_id'])
channel_id = int(data['channel_id'])
self.dispatch('raw_reaction_clear', message_id, channel_id)
message = self._get_message(message_id)
if message is not None: if message is not None:
old_reactions = message.reactions.copy() old_reactions = message.reactions.copy()
message.reactions.clear() message.reactions.clear()
self.dispatch('reaction_clear', message, old_reactions) self.dispatch('reaction_clear', message, old_reactions)
def parse_message_reaction_remove(self, data): def parse_message_reaction_remove(self, data):
message = self._get_message(int(data['message_id'])) message_id = int(data['message_id'])
user_id = int(data['user_id'])
channel_id = int(data['channel_id'])
emoji_data = data['emoji']
emoji_id = utils._get_as_snowflake(emoji_data, 'id')
emoji = PartialReactionEmoji(id=emoji_id, name=emoji_data['name'])
self.dispatch('raw_reaction_remove', emoji, message_id, channel_id, user_id)
message = self._get_message(message_id)
if message is not None: if message is not None:
emoji = self._upgrade_partial_emoji(emoji)
try: try:
reaction = message._remove_reaction(data) reaction = message._remove_reaction(data, emoji, user_id)
except (AttributeError, ValueError) as e: # eventual consistency lol except (AttributeError, ValueError) as e: # eventual consistency lol
pass pass
else: else:
user = self._get_reaction_user(message.channel, int(data['user_id'])) user = self._get_reaction_user(message.channel, user_id)
if user: if user:
self.dispatch('reaction_remove', reaction, user) self.dispatch('reaction_remove', reaction, user)
@ -790,7 +823,13 @@ class ConnectionState:
try: try:
return self._emojis[emoji_id] return self._emojis[emoji_id]
except KeyError: except KeyError:
return PartialEmoji(id=emoji_id, name=data['name']) return PartialReactionEmoji(id=emoji_id, name=data['name'])
def _upgrade_partial_emoji(self, emoji):
try:
return self._emojis[emoji.id]
except KeyError:
return emoji
def get_channel(self, id): def get_channel(self, id):
if id is None: if id is None:

4
discord/utils.py

@ -237,9 +237,7 @@ def _get_as_snowflake(data, key):
except KeyError: except KeyError:
return None return None
else: else:
if value is None: return value and int(value)
return value
return int(value)
def _get_mime_type_for_image(data): def _get_mime_type_for_image(data):
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):

71
docs/api.rst

@ -232,6 +232,23 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param message: A :class:`Message` of the deleted message. :param message: A :class:`Message` of the deleted message.
.. function:: on_raw_message_delete(message_id, channel_id)
Called when a message is deleted. Unlike :func:`on_message_delete`, this is
called regardless of the message being in the internal message cache or not.
:param int message_id: The message ID of the message being deleted.
:param int channel_id: The channel ID where the message was deleted.
.. function:: on_raw_bulk_message_delete(message_ids, channel_id)
Called when a bulk delete is triggered. This event is called regardless
of the message IDs being in the internal message cache or not.
:param message_ids: The message IDs that were bulk deleted.
:type message_ids: Set[int]
:param int channel_id: The channel ID where the messages were deleted.
.. function:: on_message_edit(before, after) .. function:: on_message_edit(before, after)
Called when a :class:`Message` receives an update event. If the message is not found Called when a :class:`Message` receives an update event. If the message is not found
@ -252,6 +269,22 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param before: A :class:`Message` of the previous version of the message. :param before: A :class:`Message` of the previous version of the message.
:param after: A :class:`Message` of the current version of the message. :param after: A :class:`Message` of the current version of the message.
.. function:: on_raw_message_edit(message_id, data)
Called when a message is edited. Unlike :func:`on_message_edit`, this is called
regardless of the state of the internal message cache.
Due to the inherently raw nature of this event, the data parameter coincides with
the raw data given by the `gateway <https://discordapp.com/developers/docs/topics/gateway#message-update>`_
Since the data payload can be partial, care must be taken when accessing stuff in the dictionary.
One example of a common case of partial data is when the ``'content'`` key is inaccessible. This
denotes an "embed" only edit, which is an edit in which only the embeds are updated by the Discord
embed server.
:param int message_id: The message ID of the message being edited.
:param dict data: The raw data being passed to the MESSAGE_UPDATE gateway event.
.. function:: on_reaction_add(reaction, user) .. function:: on_reaction_add(reaction, user)
Called when a message has a reaction added to it. Similar to on_message_edit, Called when a message has a reaction added to it. Similar to on_message_edit,
@ -265,6 +298,17 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param reaction: A :class:`Reaction` showing the current state of the reaction. :param reaction: A :class:`Reaction` showing the current state of the reaction.
:param user: A :class:`User` or :class:`Member` of the user who added the reaction. :param user: A :class:`User` or :class:`Member` of the user who added the reaction.
.. function:: on_raw_reaction_add(emoji, message_id, channel_id, user_id)
Called when a reaction has a reaction added. Unlike :func:`on_reaction_add`, this is
called regardless of the state of the internal message cache.
:param emoji: The custom or unicode emoji being reacted to.
:type emoji: :class:`PartialReactionEmoji`
:param int message_id: The message ID of the message being reacted.
:param int channel_id: The channel ID where the message belongs to.
:param int user_id: The user ID of the user who did the reaction.
.. function:: on_reaction_remove(reaction, user) .. function:: on_reaction_remove(reaction, user)
Called when a message has a reaction removed from it. Similar to on_message_edit, Called when a message has a reaction removed from it. Similar to on_message_edit,
@ -278,15 +322,34 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param reaction: A :class:`Reaction` showing the current state of the reaction. :param reaction: A :class:`Reaction` showing the current state of the reaction.
:param user: A :class:`User` or :class:`Member` of the user who removed the reaction. :param user: A :class:`User` or :class:`Member` of the user who removed the reaction.
.. function:: on_raw_reaction_remove(emoji, message_id, channel_id, user_id)
Called when a reaction has a reaction removed. Unlike :func:`on_reaction_remove`, this is
called regardless of the state of the internal message cache.
:param emoji: The custom or unicode emoji that got un-reacted.
:type emoji: :class:`PartialReactionEmoji`
:param int message_id: The message ID of the message being un-reacted.
:param int channel_id: The channel ID where the message belongs to.
:param int user_id: The user ID of the user who removed the reaction.
.. function:: on_reaction_clear(message, reactions) .. function:: on_reaction_clear(message, reactions)
Called when a message has all its reactions removed from it. Similar to on_message_edit, Called when a message has all its reactions removed from it. Similar to :func:`on_message_edit`,
if the message is not found in the :attr:`Client.messages` cache, then this event if the message is not found in the :attr:`Client.messages` cache, then this event
will not be called. will not be called.
:param message: The :class:`Message` that had its reactions cleared. :param message: The :class:`Message` that had its reactions cleared.
:param reactions: A list of :class:`Reaction`\s that were removed. :param reactions: A list of :class:`Reaction`\s that were removed.
.. function:: on_raw_reaction_clear(message_id, channel_id)
Called when a message has all its reactions removed. Unlike :func:`on_reaction_clear`,
this is called regardless of the state of the internal message cache.
:param int message_id: The message ID of the message having its reactions removed.
:param int channel_id: The channel ID of where the message belongs to.
.. function:: on_private_channel_delete(channel) .. function:: on_private_channel_delete(channel)
on_private_channel_create(channel) on_private_channel_create(channel)
@ -1687,6 +1750,12 @@ Emoji
.. autoclass:: Emoji .. autoclass:: Emoji
:members: :members:
PartialReactionEmoji
~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: PartialReactionEmoji
:members:
Role Role
~~~~~ ~~~~~

Loading…
Cancel
Save