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:
parent
158ac6bb50
commit
c4acc0e1a1
@ -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
|
||||
|
20
docs/api.rst
20
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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user