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.
This commit is contained in:
Rapptz
2017-06-09 18:36:59 -04:00
parent 1be119cd45
commit d239cc2666
7 changed files with 178 additions and 26 deletions

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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')

View File

@ -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:

View File

@ -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'):