From d239cc26666ff255a0c86c83541ef90f2b586598 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 9 Jun 2017 18:36:59 -0400 Subject: 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 +--- 6 files changed, 108 insertions(+), 25 deletions(-) (limited to 'discord') diff --git a/discord/__init__.py b/discord/__init__.py index 32ff5b02..5efdd74a 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 8c880b5d..0c161c6c 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 2550798f..27296117 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 833f1b3d..91c606d9 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 aaa3aa54..67f8f763 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 35044836..06aa5a35 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'): -- cgit v1.2.3