aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkhazhyk <[email protected]>2016-10-26 21:34:28 -0700
committerkhazhyk <[email protected]>2016-10-27 21:36:32 -0700
commitc4acc0e1a169cebf2adbf974df802ac4b802efec (patch)
tree39e30d0ed4093118bf8a76663e9ad5f0937528a7
parentAdd around parameter to LogsFromIterator. (diff)
downloaddiscord.py-c4acc0e1a169cebf2adbf974df802ac4b802efec.tar.xz
discord.py-c4acc0e1a169cebf2adbf974df802ac4b802efec.zip
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
-rw-r--r--discord/client.py125
-rw-r--r--discord/http.py18
-rw-r--r--discord/message.py6
-rw-r--r--discord/permissions.py15
-rw-r--r--discord/reaction.py88
-rw-r--r--discord/state.py55
-rw-r--r--docs/api.rst20
7 files changed, 323 insertions, 4 deletions
diff --git a/discord/client.py b/discord/client.py
index c3594638..ff247959 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -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
@@ -776,6 +777,130 @@ class Client:
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|
diff --git a/discord/http.py b/discord/http.py
index 26824fcb..01d17d55 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -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_())
diff --git a/discord/message.py b/discord/message.py
index 0413424e..d2bdf87e 100644
--- a/discord/message.py
+++ b/discord/message.py
@@ -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__)
diff --git a/discord/permissions.py b/discord/permissions.py
index 0fc69c5c..e6a1df58 100644
--- a/discord/permissions.py
+++ b/discord/permissions.py
@@ -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
diff --git a/discord/reaction.py b/discord/reaction.py
new file mode 100644
index 00000000..ec30fa22
--- /dev/null
+++ b/discord/reaction.py
@@ -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)
diff --git a/discord/state.py b/discord/state.py
index 747c3bd9..4d3855fc 100644
--- a/discord/state.py
+++ b/discord/state.py
@@ -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
diff --git a/docs/api.rst b/docs/api.rst
index 641331c4..72162f51 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -207,6 +207,26 @@ to handle it, which defaults to print a traceback and ignore 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_message_reaction_add(message, reaction, user)
+
+ Called when a message has a reaction added to it. Similar to on_message_edit,
+ if the message is not found in the :attr:`Client.messages` cache, then this
+ event will not be called.
+
+ :param message: A :class:`Message` that was reacted to.
+ :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_message_reaction_remove(message, reaction, user)
+
+ Called when a message has a reaction removed from it. Similar to on_message_edit,
+ if the message is not found in the :attr:`Client.messages` cache, then this event
+ will not be called.
+
+ :param message: A :class:`Message` that was reacted to.
+ :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_channel_delete(channel)
on_channel_create(channel)