From d239cc26666ff255a0c86c83541ef90f2b586598 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 9 Jun 2017 18:36:59 -0400 Subject: [PATCH] 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. --- discord/__init__.py | 2 +- discord/emoji.py | 50 ++++++++++++++++++++++++++++++- discord/message.py | 10 +++---- discord/reaction.py | 2 +- discord/state.py | 65 ++++++++++++++++++++++++++++++++--------- discord/utils.py | 4 +-- docs/api.rst | 71 ++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 178 insertions(+), 26 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 32ff5b020..5efdd74a1 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -20,7 +20,7 @@ __version__ = '1.0.0a' from .client import Client, AppInfo from .user import User, ClientUser, Profile from .game import Game -from .emoji import Emoji, PartialEmoji +from .emoji import Emoji, PartialReactionEmoji from .channel import * from .guild import Guild from .relationship import Relationship diff --git a/discord/emoji.py b/discord/emoji.py index 8c880b5d1..0c161c6c0 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -30,7 +30,55 @@ from collections import namedtuple from . import utils 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): """Represents a custom emoji. diff --git a/discord/message.py b/discord/message.py index 2550798f2..272961178 100644 --- a/discord/message.py +++ b/discord/message.py @@ -198,10 +198,9 @@ class Message: else: setattr(self, key, transform(value)) - def _add_reaction(self, data): - emoji = self._state.get_reaction_emoji(data['emoji']) + def _add_reaction(self, data, emoji, user_id): 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: reaction = Reaction(message=self, data=data, emoji=emoji) @@ -213,8 +212,7 @@ class Message: return reaction - def _remove_reaction(self, data): - emoji = self._state.get_reaction_emoji(data['emoji']) + def _remove_reaction(self, data, emoji, user_id): reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) if reaction is None: @@ -225,7 +223,7 @@ class Message: # sent bad data, or we stored improperly reaction.count -= 1 - if int(data['user_id']) == self._state.self_id: + if user_id == self._state.self_id: reaction.me = False if reaction.count == 0: # this raises ValueError if something went wrong as well. diff --git a/discord/reaction.py b/discord/reaction.py index 833f1b3d3..91c606d9e 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -67,7 +67,7 @@ class Reaction: def __init__(self, *, message, data, emoji=None): 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.me = data.get('me') diff --git a/discord/state.py b/discord/state.py index aaa3aa547..67f8f7639 100644 --- a/discord/state.py +++ b/discord/state.py @@ -26,7 +26,7 @@ DEALINGS IN THE SOFTWARE. from .guild import Guild from .user import User, ClientUser -from .emoji import Emoji, PartialEmoji +from .emoji import Emoji, PartialReactionEmoji from .message import Message from .relationship import Relationship from .channel import * @@ -35,6 +35,7 @@ from .role import Role from .enums import ChannelType, try_enum, Status from .calls import GroupCall from . import utils, compat +from .embeds import Embed from collections import deque, namedtuple import copy, enum, math @@ -314,20 +315,27 @@ class ConnectionState: def parse_message_delete(self, data): 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) if found is not None: self.dispatch('message_delete', found) self._messages.remove(found) def parse_message_delete_bulk(self, data): - message_ids = set(map(int, data.get('ids', []))) - to_be_deleted = list(filter(lambda m: m.id in message_ids, self._messages)) + message_ids = { int(x) for x in data.get('ids', []) } + 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: self.dispatch('message_delete', msg) self._messages.remove(msg) 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: older_message = copy.copy(message) if 'call' in data: @@ -335,36 +343,61 @@ class ConnectionState: message._handle_call(data['call']) elif 'content' not in data: # embed only edit - message.embeds = data['embeds'] + message.embeds = [Embed.from_data(d) for d in data['embeds']] else: message._update(channel=message.channel, data=data) self.dispatch('message_edit', older_message, message) 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: - reaction = message._add_reaction(data) - user = self._get_reaction_user(message.channel, int(data['user_id'])) + emoji = self._upgrade_partial_emoji(emoji) + reaction = message._add_reaction(data, emoji, user_id) + user = self._get_reaction_user(message.channel, user_id) if user: self.dispatch('reaction_add', reaction, user) 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: old_reactions = message.reactions.copy() message.reactions.clear() self.dispatch('reaction_clear', message, old_reactions) 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: + emoji = self._upgrade_partial_emoji(emoji) try: - reaction = message._remove_reaction(data) + reaction = message._remove_reaction(data, emoji, user_id) except (AttributeError, ValueError) as e: # eventual consistency lol pass else: - user = self._get_reaction_user(message.channel, int(data['user_id'])) + user = self._get_reaction_user(message.channel, user_id) if user: self.dispatch('reaction_remove', reaction, user) @@ -790,7 +823,13 @@ class ConnectionState: try: return self._emojis[emoji_id] 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): if id is None: diff --git a/discord/utils.py b/discord/utils.py index 350448369..06aa5a35c 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -237,9 +237,7 @@ def _get_as_snowflake(data, key): except KeyError: return None else: - if value is None: - return value - return int(value) + return value and int(value) def _get_mime_type_for_image(data): if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): diff --git a/docs/api.rst b/docs/api.rst index cb0716a0a..53ce40515 100644 --- a/docs/api.rst +++ b/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. +.. 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) 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 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 `_ + + 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) 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 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) 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 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) - 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 will not be called. :param message: The :class:`Message` that had its reactions cleared. :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) on_private_channel_create(channel) @@ -1687,6 +1750,12 @@ Emoji .. autoclass:: Emoji :members: +PartialReactionEmoji +~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: PartialReactionEmoji + :members: + Role ~~~~~