Add support for reactions.
Reactions can be be standard emojis, or custom server emojis. Adds - add/remove_reaction - get_reaction_users - Messages have new field reactions - new events - message_reaction_add, message_reaction_remove - new permission - add_reactions
This commit is contained in:
		| @@ -32,6 +32,7 @@ from .server import Server | ||||
| from .message import Message | ||||
| from .invite import Invite | ||||
| from .object import Object | ||||
| from .reaction import Reaction | ||||
| from .role import Role | ||||
| from .errors import * | ||||
| from .state import ConnectionState | ||||
| @@ -775,6 +776,130 @@ class Client: | ||||
|         self.connection._add_private_channel(channel) | ||||
|         return channel | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def add_reaction(self, message, emoji): | ||||
|         """|coro| | ||||
|  | ||||
|         Add a reaction to the given message. | ||||
|  | ||||
|         The message must be a :class:`Message` that exists. emoji may be a unicode emoji, | ||||
|         or a custom server :class:`Emoji`. | ||||
|  | ||||
|         Parameters | ||||
|         ------------ | ||||
|         message : :class:`Message` | ||||
|             The message to react to. | ||||
|         emoji : :class:`Emoji` or str | ||||
|             The emoji to react with. | ||||
|  | ||||
|         Raises | ||||
|         -------- | ||||
|         HTTPException | ||||
|             Adding the reaction failed. | ||||
|         Forbidden | ||||
|             You do not have the proper permissions to react to the message. | ||||
|         NotFound | ||||
|             The message or emoji you specified was not found. | ||||
|         InvalidArgument | ||||
|             The message or emoji parameter is invalid. | ||||
|         """ | ||||
|         if not isinstance(message, Message): | ||||
|             raise InvalidArgument('message argument must be a Message') | ||||
|         if not isinstance(emoji, (str, Emoji)): | ||||
|             raise InvalidArgument('emoji argument must be a string or Emoji') | ||||
|  | ||||
|         if isinstance(emoji, Emoji): | ||||
|             emoji = '{}:{}'.format(emoji.name, emoji.id) | ||||
|  | ||||
|         yield from self.http.add_reaction(message.id, message.channel.id, emoji) | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def remove_reaction(self, message, emoji, member): | ||||
|         """|coro| | ||||
|  | ||||
|         Remove a reaction by the member from the given message. | ||||
|  | ||||
|         If member != server.me, you need Manage Messages to remove the reaction. | ||||
|  | ||||
|         The message must be a :class:`Message` that exists. emoji may be a unicode emoji, | ||||
|         or a custom server :class:`Emoji`. | ||||
|  | ||||
|         Parameters | ||||
|         ------------ | ||||
|         message : :class:`Message` | ||||
|             The message. | ||||
|         emoji : :class:`Emoji` or str | ||||
|             The emoji to remove. | ||||
|         member : :class:`Member` | ||||
|             The member for which to delete the reaction. | ||||
|  | ||||
|         Raises | ||||
|         -------- | ||||
|         HTTPException | ||||
|             Adding the reaction failed. | ||||
|         Forbidden | ||||
|             You do not have the proper permissions to remove the reaction. | ||||
|         NotFound | ||||
|             The message or emoji you specified was not found. | ||||
|         InvalidArgument | ||||
|             The message or emoji parameter is invalid. | ||||
|         """ | ||||
|         if not isinstance(message, Message): | ||||
|             raise InvalidArgument('message argument must be a Message') | ||||
|         if not isinstance(emoji, (str, Emoji)): | ||||
|             raise InvalidArgument('emoji must be a string or Emoji') | ||||
|  | ||||
|         if isinstance(emoji, Emoji): | ||||
|             emoji = '{}:{}'.format(emoji.name, emoji.id) | ||||
|  | ||||
|         if member == self.user: | ||||
|             member_id = '@me' | ||||
|         else: | ||||
|             member_id = member.id | ||||
|  | ||||
|         yield from self.http.remove_reaction(message.id, message.channel.id, emoji, member_id) | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def get_reaction_users(self, reaction, limit=100, after=None): | ||||
|         """|coro| | ||||
|  | ||||
|         Get the users that added a reaction to a message. | ||||
|  | ||||
|         Parameters | ||||
|         ------------ | ||||
|         reaction : :class:`Reaction` | ||||
|             The reaction to retrieve users for. | ||||
|         limit : int | ||||
|             The maximum number of results to return. | ||||
|         after : :class:`Member` or :class:`Object` | ||||
|             For pagination, reactions are sorted by member. | ||||
|  | ||||
|         Raises | ||||
|         -------- | ||||
|         HTTPException | ||||
|             Getting the users for the reaction failed. | ||||
|         NotFound | ||||
|             The message or emoji you specified was not found. | ||||
|         InvalidArgument | ||||
|             The reaction parameter is invalid. | ||||
|         """ | ||||
|         if not isinstance(reaction, Reaction): | ||||
|             raise InvalidArgument('reaction must be a Reaction') | ||||
|  | ||||
|         emoji = reaction.emoji | ||||
|  | ||||
|         if isinstance(emoji, Emoji): | ||||
|             emoji = '{}:{}'.format(emoji.name, emoji.id) | ||||
|  | ||||
|         if after: | ||||
|             after = after.id | ||||
|  | ||||
|         data = yield from self.http.get_reaction_users( | ||||
|             reaction.message.id, reaction.message.channel.id, | ||||
|             emoji, limit, after=after) | ||||
|  | ||||
|         return [User(**user) for user in data] | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def send_message(self, destination, content, *, tts=False): | ||||
|         """|coro| | ||||
|   | ||||
| @@ -261,6 +261,24 @@ class HTTPClient: | ||||
|         } | ||||
|         return self.patch(url, json=payload, bucket='messages:' + str(guild_id)) | ||||
|  | ||||
|     def add_reaction(self, message_id, channel_id, emoji): | ||||
|         url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}/@me'.format( | ||||
|             self, channel_id, message_id, emoji) | ||||
|         return self.put(url, bucket=_func_()) | ||||
|  | ||||
|     def remove_reaction(self, message_id, channel_id, emoji, member_id): | ||||
|         url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}/{4}'.format( | ||||
|             self, channel_id, message_id, emoji, member_id) | ||||
|         return self.delete(url, bucket=_func_()) | ||||
|  | ||||
|     def get_reaction_users(self, message_id, channel_id, emoji, limit, after=None): | ||||
|         url = '{0.CHANNELS}/{1}/messages/{2}/reactions/{3}'.format( | ||||
|             self, channel_id, message_id, emoji) | ||||
|         params = {'limit': limit} | ||||
|         if after: | ||||
|             params['after'] = after | ||||
|         return self.get(url, params=params, bucket=_func_()) | ||||
|  | ||||
|     def get_message(self, channel_id, message_id): | ||||
|         url = '{0.CHANNELS}/{1}/messages/{2}'.format(self, channel_id, message_id) | ||||
|         return self.get(url, bucket=_func_()) | ||||
|   | ||||
| @@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE. | ||||
|  | ||||
| from . import utils | ||||
| from .user import User | ||||
| from .reaction import Reaction | ||||
| from .object import Object | ||||
| from .calls import CallMessage | ||||
| import re | ||||
| @@ -102,6 +103,8 @@ class Message: | ||||
|         A list of attachments given to a message. | ||||
|     pinned: bool | ||||
|         Specifies if the message is currently pinned. | ||||
|     reactions : List[:class:`Reaction`] | ||||
|         Reactions to a message. Reactions can be either custom emoji or standard unicode emoji. | ||||
|     """ | ||||
|  | ||||
|     __slots__ = [ 'edited_timestamp', 'timestamp', 'tts', 'content', 'channel', | ||||
| @@ -109,7 +112,7 @@ class Message: | ||||
|                   'channel_mentions', 'server', '_raw_mentions', 'attachments', | ||||
|                   '_clean_content', '_raw_channel_mentions', 'nonce', 'pinned', | ||||
|                   'role_mentions', '_raw_role_mentions', 'type', 'call', | ||||
|                   '_system_content' ] | ||||
|                   '_system_content', 'reactions' ] | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         self._update(**kwargs) | ||||
| @@ -135,6 +138,7 @@ class Message: | ||||
|         self._handle_upgrades(data.get('channel_id')) | ||||
|         self._handle_mentions(data.get('mentions', []), data.get('mention_roles', [])) | ||||
|         self._handle_call(data.get('call')) | ||||
|         self.reactions = [Reaction(message=self, **reaction) for reaction in data.get('reactions', [])] | ||||
|  | ||||
|         # clear the cached properties | ||||
|         cached = filter(lambda attr: attr[0] == '_', self.__slots__) | ||||
|   | ||||
| @@ -127,7 +127,7 @@ class Permissions: | ||||
|     def all(cls): | ||||
|         """A factory method that creates a :class:`Permissions` with all | ||||
|         permissions set to True.""" | ||||
|         return cls(0b01111111111101111111110000111111) | ||||
|         return cls(0b01111111111101111111110001111111) | ||||
|  | ||||
|     @classmethod | ||||
|     def all_channel(cls): | ||||
| @@ -142,7 +142,7 @@ class Permissions: | ||||
|         - change_nicknames | ||||
|         - manage_nicknames | ||||
|         """ | ||||
|         return cls(0b00110011111101111111110000010001) | ||||
|         return cls(0b00110011111101111111110001010001) | ||||
|  | ||||
|     @classmethod | ||||
|     def general(cls): | ||||
| @@ -154,7 +154,7 @@ class Permissions: | ||||
|     def text(cls): | ||||
|         """A factory method that creates a :class:`Permissions` with all | ||||
|         "Text" permissions from the official Discord UI set to True.""" | ||||
|         return cls(0b00000000000001111111110000000000) | ||||
|         return cls(0b00000000000001111111110001000000) | ||||
|  | ||||
|     @classmethod | ||||
|     def voice(cls): | ||||
| @@ -248,6 +248,15 @@ class Permissions: | ||||
|     def manage_server(self, value): | ||||
|         self._set(5, value) | ||||
|  | ||||
|     @property | ||||
|     def add_reactions(self): | ||||
|         """Returns True if a user can add reactions to messages.""" | ||||
|         return self._bit(6) | ||||
|  | ||||
|     @add_reactions.setter | ||||
|     def add_reactions(self, value): | ||||
|         self._set(6, value) | ||||
|  | ||||
|     # 4 unused | ||||
|  | ||||
|     @property | ||||
|   | ||||
							
								
								
									
										88
									
								
								discord/reaction.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								discord/reaction.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| The MIT License (MIT) | ||||
|  | ||||
| Copyright (c) 2015-2016 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. | ||||
| """ | ||||
|  | ||||
| from .emoji import Emoji | ||||
|  | ||||
| class Reaction: | ||||
|     """Represents a reaction to a message. | ||||
|  | ||||
|     Depending on the way this object was created, some of the attributes can | ||||
|     have a value of ``None``. | ||||
|  | ||||
|     Similar to members, the same reaction to a different message are equal. | ||||
|  | ||||
|     Supported Operations: | ||||
|  | ||||
|     +-----------+-------------------------------------------+ | ||||
|     | Operation |               Description                 | | ||||
|     +===========+===========================================+ | ||||
|     | x == y    | Checks if two reactions are the same.     | | ||||
|     +-----------+-------------------------------------------+ | ||||
|     | x != y    | Checks if two reactions are not the same. | | ||||
|     +-----------+-------------------------------------------+ | ||||
|     | hash(x)   | Return the emoji's hash.                  | | ||||
|     +-----------+-------------------------------------------+ | ||||
|  | ||||
|     Attributes | ||||
|     ----------- | ||||
|     emoji : :class:`Emoji` or str | ||||
|         The reaction emoji. May be a custom emoji, or a unicode emoji. | ||||
|     custom_emoji : bool | ||||
|         If this is a custom emoji. | ||||
|     count : int | ||||
|         Number of times this reaction was made | ||||
|     me : bool | ||||
|         If the user has send this reaction. | ||||
|     message: :class:`Message` | ||||
|         Message this reaction is for. | ||||
|     """ | ||||
|     __slots__ = ['message', 'count', 'emoji', 'me', 'custom_emoji'] | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         self.message = kwargs.pop('message') | ||||
|         self._from_data(kwargs) | ||||
|  | ||||
|     def _from_data(self, reaction): | ||||
|         self.count = reaction.get('count', 1) | ||||
|         self.me = reaction.get('me') | ||||
|         emoji = reaction['emoji'] | ||||
|         if emoji['id']: | ||||
|             self.custom_emoji = True | ||||
|             self.emoji = Emoji(server=None, id=emoji['id'], name=emoji['name']) | ||||
|         else: | ||||
|             self.custom_emoji = False | ||||
|             self.emoji = emoji['name'] | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return isinstance(other, self.__class__) and other.emoji == self.emoji | ||||
|  | ||||
|     def __ne__(self, other): | ||||
|         if isinstance(other, self.__class__): | ||||
|             return other.emoji != self.emoji | ||||
|         return True | ||||
|  | ||||
|     def __hash__(self): | ||||
|         return hash(self.emoji) | ||||
| @@ -28,6 +28,7 @@ from .server import Server | ||||
| from .user import User | ||||
| from .game import Game | ||||
| from .emoji import Emoji | ||||
| from .reaction import Reaction | ||||
| from .message import Message | ||||
| from .channel import Channel, PrivateChannel | ||||
| from .member import Member | ||||
| @@ -251,6 +252,54 @@ class ConnectionState: | ||||
|  | ||||
|             self.dispatch('message_edit', older_message, message) | ||||
|  | ||||
|     def parse_message_reaction_add(self, data): | ||||
|         message = self._get_message(data['message_id']) | ||||
|         if message is not None: | ||||
|             if data['emoji']['id']: | ||||
|                 reaction_emoji = Emoji(server=None, **data['emoji']) | ||||
|             else: | ||||
|                 reaction_emoji = data['emoji']['name'] | ||||
|             reaction = utils.get( | ||||
|                 message.reactions, emoji=reaction_emoji) | ||||
|  | ||||
|             is_me = data['user_id'] == self.user.id | ||||
|  | ||||
|             if not reaction: | ||||
|                 reaction = Reaction(message=message, me=is_me, **data) | ||||
|                 message.reactions.append(reaction) | ||||
|             else: | ||||
|                 reaction.count += 1 | ||||
|                 if is_me: | ||||
|                     reaction.me = True | ||||
|  | ||||
|             channel = self.get_channel(data['channel_id']) | ||||
|             member = self._get_member(channel, data['user_id']) | ||||
|  | ||||
|             self.dispatch('message_reaction_add', message, reaction, member) | ||||
|  | ||||
|     def parse_message_reaction_remove(self, data): | ||||
|         message = self._get_message(data['message_id']) | ||||
|         if message is not None: | ||||
|             if data['emoji']['id']: | ||||
|                 reaction_emoji = Emoji(server=None, **data['emoji']) | ||||
|             else: | ||||
|                 reaction_emoji = data['emoji']['name'] | ||||
|             reaction = utils.get( | ||||
|                 message.reactions, emoji=reaction_emoji) | ||||
|  | ||||
|             # if reaction isn't in the list, we crash. This means discord | ||||
|             # sent bad data, or we stored improperly | ||||
|             reaction.count -= 1 | ||||
|             if data['user_id'] == self.user.id: | ||||
|                 reaction.me = False | ||||
|             if reaction.count == 0: | ||||
|                 message.reactions.remove(reaction) | ||||
|  | ||||
|             channel = self.get_channel(data['channel_id']) | ||||
|             member = self._get_member(channel, data['user_id']) | ||||
|  | ||||
|             self.dispatch('message_reaction_remove', message, reaction, member) | ||||
|  | ||||
|     def parse_presence_update(self, data): | ||||
|         server = self._get_server(data.get('guild_id')) | ||||
|         if server is None: | ||||
| @@ -625,6 +674,12 @@ class ConnectionState: | ||||
|         if call is not None: | ||||
|             self.dispatch('call_remove', call) | ||||
|  | ||||
|     def _get_member(self, channel, id): | ||||
|         if channel.is_private: | ||||
|             return utils.get(channel.recipients, id=id) | ||||
|         else: | ||||
|             return channel.server.get_member(id) | ||||
|  | ||||
|     def get_channel(self, id): | ||||
|         if id is None: | ||||
|             return None | ||||
|   | ||||
		Reference in New Issue
	
	Block a user