Add support for rich embeds.
This commit is contained in:
parent
a7ba0bb7c4
commit
af46718460
@ -37,6 +37,7 @@ from . import utils, opus, compat
|
|||||||
from .voice_client import VoiceClient
|
from .voice_client import VoiceClient
|
||||||
from .enums import ChannelType, ServerRegion, Status, MessageType, VerificationLevel
|
from .enums import ChannelType, ServerRegion, Status, MessageType, VerificationLevel
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from .embeds import Embed
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -1043,7 +1043,7 @@ class Client:
|
|||||||
return [User(**user) for user in data]
|
return [User(**user) for user in data]
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def send_message(self, destination, content, *, tts=False):
|
def send_message(self, destination, content=None, *, tts=False, embed=None):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
Sends a message to the destination given with the content given.
|
Sends a message to the destination given with the content given.
|
||||||
@ -1062,15 +1062,23 @@ class Client:
|
|||||||
``str`` being allowed was removed and replaced with :class:`Object`.
|
``str`` being allowed was removed and replaced with :class:`Object`.
|
||||||
|
|
||||||
The content must be a type that can convert to a string through ``str(content)``.
|
The content must be a type that can convert to a string through ``str(content)``.
|
||||||
|
If the content is set to ``None`` (the default), then the ``embed`` parameter must
|
||||||
|
be provided.
|
||||||
|
|
||||||
|
If the ``embed`` parameter is provided, it must be of type :class:`Embed` and
|
||||||
|
it must be a rich embed type.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
------------
|
------------
|
||||||
destination
|
destination
|
||||||
The location to send the message.
|
The location to send the message.
|
||||||
content
|
content
|
||||||
The content of the message to send.
|
The content of the message to send. If this is missing,
|
||||||
|
then the ``embed`` parameter must be present.
|
||||||
tts : bool
|
tts : bool
|
||||||
Indicates if the message should be sent using text-to-speech.
|
Indicates if the message should be sent using text-to-speech.
|
||||||
|
embed: :class:`Embed`
|
||||||
|
The rich embed for the content.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
--------
|
--------
|
||||||
@ -1083,6 +1091,25 @@ class Client:
|
|||||||
InvalidArgument
|
InvalidArgument
|
||||||
The destination parameter is invalid.
|
The destination parameter is invalid.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
----------
|
||||||
|
|
||||||
|
Sending a regular message:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client.send_message(message.channel, 'Hello')
|
||||||
|
|
||||||
|
Sending a TTS message:
|
||||||
|
|
||||||
|
await client.send_message(message.channel, 'Goodbye.', tts=True)
|
||||||
|
|
||||||
|
Sending an embed message:
|
||||||
|
|
||||||
|
em = discord.Embed(title='My Embed Title', description='My Embed Content.', colour=0xDEADBF)
|
||||||
|
em.set_author(name='Someone', icon_url=client.user.default_avatar_url)
|
||||||
|
await client.send_message(message.channel, embed=em)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
---------
|
---------
|
||||||
:class:`Message`
|
:class:`Message`
|
||||||
@ -1091,9 +1118,12 @@ class Client:
|
|||||||
|
|
||||||
channel_id, guild_id = yield from self._resolve_destination(destination)
|
channel_id, guild_id = yield from self._resolve_destination(destination)
|
||||||
|
|
||||||
content = str(content)
|
content = str(content) if content else None
|
||||||
|
|
||||||
data = yield from self.http.send_message(channel_id, content, guild_id=guild_id, tts=tts)
|
if embed is not None:
|
||||||
|
embed = embed.to_dict()
|
||||||
|
|
||||||
|
data = yield from self.http.send_message(channel_id, content, guild_id=guild_id, tts=tts, embed=embed)
|
||||||
channel = self.get_channel(data.get('channel_id'))
|
channel = self.get_channel(data.get('channel_id'))
|
||||||
message = self.connection._create_message(channel=channel, **data)
|
message = self.connection._create_message(channel=channel, **data)
|
||||||
return message
|
return message
|
||||||
|
398
discord/embeds.py
Normal file
398
discord/embeds.py
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from .colour import Colour
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
class _EmptyEmbed:
|
||||||
|
def __bool__(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Embed.Empty'
|
||||||
|
|
||||||
|
EmptyEmbed = _EmptyEmbed()
|
||||||
|
|
||||||
|
class EmbedProxy:
|
||||||
|
def __init__(self, layer):
|
||||||
|
self.__dict__.update(layer)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'EmbedProxy(%s)' % ', '.join(('%s=%r' % (k, v) for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return EmptyEmbed
|
||||||
|
|
||||||
|
class Embed:
|
||||||
|
"""Represents a Discord embed.
|
||||||
|
|
||||||
|
The following attributes can be set during creation
|
||||||
|
of the object:
|
||||||
|
|
||||||
|
Certain properties return an ``EmbedProxy``. Which is a type
|
||||||
|
that acts similar to a regular `dict` except access the attributes
|
||||||
|
via dotted access, e.g. ``embed.author.icon_url``. If the attribute
|
||||||
|
is invalid or empty, then a special sentinel value is returned,
|
||||||
|
:attr:`Embed.Empty`.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
title: str
|
||||||
|
The title of the embed.
|
||||||
|
type: str
|
||||||
|
The type of embed. Usually "rich".
|
||||||
|
description: str
|
||||||
|
The description of the embed.
|
||||||
|
url: str
|
||||||
|
The URL of the embed.
|
||||||
|
timestamp: `datetime.datetime`
|
||||||
|
The timestamp of the embed content.
|
||||||
|
colour: :class:`Colour` or int
|
||||||
|
The colour code of the embed. Aliased to ``color`` as well.
|
||||||
|
Empty
|
||||||
|
A special sentinel value used by ``EmbedProxy`` to denote
|
||||||
|
that the value or attribute is empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
|
||||||
|
'_image', '_thumbnail', '_video', '_provider', '_author',
|
||||||
|
'_fields', 'description')
|
||||||
|
|
||||||
|
Empty = EmptyEmbed
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# swap the colour/color aliases
|
||||||
|
try:
|
||||||
|
colour = kwargs['colour']
|
||||||
|
except KeyError:
|
||||||
|
colour = kwargs.get('color')
|
||||||
|
|
||||||
|
if colour is not None:
|
||||||
|
self.colour = colour
|
||||||
|
|
||||||
|
self.title = kwargs.get('title')
|
||||||
|
self.type = kwargs.get('type', 'rich')
|
||||||
|
self.url = kwargs.get('url')
|
||||||
|
self.description = kwargs.get('description')
|
||||||
|
|
||||||
|
try:
|
||||||
|
timestamp = kwargs['timestamp']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.timestamp = timestamp
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data):
|
||||||
|
# we are bypassing __init__ here since it doesn't apply here
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
|
||||||
|
# fill in the basic fields
|
||||||
|
|
||||||
|
self.title = data.get('title')
|
||||||
|
self.type = data.get('type')
|
||||||
|
self.description = data.get('description')
|
||||||
|
self.url = data.get('url')
|
||||||
|
|
||||||
|
# try to fill in the more rich fields
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._colour = Colour(value=data['color'])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._timestamp = utils.parse_time(data['timestamp'])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for attr in ('thumbnail', 'video', 'provider', 'author', 'fields'):
|
||||||
|
try:
|
||||||
|
value = data[attr]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
setattr(self, '_' + attr, value)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def colour(self):
|
||||||
|
return getattr(self, '_colour', None)
|
||||||
|
|
||||||
|
@colour.setter
|
||||||
|
def colour(self, value):
|
||||||
|
if isinstance(value, Colour):
|
||||||
|
self._colour = value
|
||||||
|
elif isinstance(value, int):
|
||||||
|
self._colour = Colour(value=value)
|
||||||
|
else:
|
||||||
|
raise TypeError('Expected discord.Colour or int, received %s instead.' % value.__class__.__name__)
|
||||||
|
|
||||||
|
color = colour
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
return getattr(self, '_timestamp', None)
|
||||||
|
|
||||||
|
@timestamp.setter
|
||||||
|
def timestamp(self, value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
self._timestamp = value
|
||||||
|
else:
|
||||||
|
raise TypeError("Expected datetime.datetime received %s instead" % value.__class__.__name__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def footer(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the footer contents.
|
||||||
|
|
||||||
|
See :meth:`set_footer` for possible values you can access.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_footer', {}))
|
||||||
|
|
||||||
|
def set_footer(self, *, text=None, icon_url=None):
|
||||||
|
"""Sets the footer for the embed content.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
text: str
|
||||||
|
The footer text.
|
||||||
|
icon_url: str
|
||||||
|
The URL of the footer icon. Only HTTP(S) is supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._footer = {}
|
||||||
|
if text is not None:
|
||||||
|
self._footer['text'] = text
|
||||||
|
|
||||||
|
if icon_url is not None:
|
||||||
|
self._footer['icon_url'] = icon_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the image contents.
|
||||||
|
|
||||||
|
See :meth:`set_image` for possible values you can access.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_image', {}))
|
||||||
|
|
||||||
|
def set_image(self, *, url, height=None, width=None):
|
||||||
|
"""Sets the image for the embed content.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
url: str
|
||||||
|
The source URL for the image. Only HTTP(S) is supported.
|
||||||
|
height: int
|
||||||
|
The height of the image.
|
||||||
|
width: int
|
||||||
|
The width of the image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._image = {
|
||||||
|
'url': url
|
||||||
|
}
|
||||||
|
|
||||||
|
if height is not None:
|
||||||
|
self._image['height'] = height
|
||||||
|
|
||||||
|
if width is not None:
|
||||||
|
self._image['width'] = width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the thumbnail contents.
|
||||||
|
|
||||||
|
See :meth:`set_thumbnail` for possible values you can access.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_thumbnail', {}))
|
||||||
|
|
||||||
|
def set_thumbnail(self, *, url, height=None, width=None):
|
||||||
|
"""Sets the thumbnail for the embed content.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
url: str
|
||||||
|
The source URL for the thumbnail. Only HTTP(S) is supported.
|
||||||
|
height: int
|
||||||
|
The height of the thumbnail.
|
||||||
|
width: int
|
||||||
|
The width of the thumbnail.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._thumbnail = {
|
||||||
|
'url': url
|
||||||
|
}
|
||||||
|
|
||||||
|
if height is not None:
|
||||||
|
self._thumbnail['height'] = height
|
||||||
|
|
||||||
|
if width is not None:
|
||||||
|
self._thumbnail['width'] = width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the video contents.
|
||||||
|
|
||||||
|
Possible attributes include:
|
||||||
|
|
||||||
|
- ``url`` for the video URL.
|
||||||
|
- ``height`` for the video height.
|
||||||
|
- ``width`` for the video width.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_video', {}))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the provider contents.
|
||||||
|
|
||||||
|
The only attributes that might be accessed are ``name`` and ``url``.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_provider', {}))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author(self):
|
||||||
|
"""Returns a ``EmbedProxy`` denoting the author contents.
|
||||||
|
|
||||||
|
See :meth:`set_author` for possible values you can access.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return EmbedProxy(getattr(self, '_author', {}))
|
||||||
|
|
||||||
|
def set_author(self, *, name, url=None, icon_url=None):
|
||||||
|
"""Sets the author for the embed content.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: str
|
||||||
|
The name of the author.
|
||||||
|
url: str
|
||||||
|
The URL for the author.
|
||||||
|
icon_url: str
|
||||||
|
The URL of the author icon. Only HTTP(S) is supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._author = {
|
||||||
|
'name': name
|
||||||
|
}
|
||||||
|
|
||||||
|
if url is not None:
|
||||||
|
self._author['url'] = url
|
||||||
|
|
||||||
|
if icon_url is not None:
|
||||||
|
self._author['icon_url'] = icon_url
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
"""Returns a list of ``EmbedProxy`` denoting the field contents.
|
||||||
|
|
||||||
|
See :meth:`add_field` for possible values you can access.
|
||||||
|
|
||||||
|
If the attribute cannot be accessed then ``None`` is returned.
|
||||||
|
"""
|
||||||
|
return [EmbedProxy(d) for d in getattr(self, '_fields', [])]
|
||||||
|
|
||||||
|
def add_field(self, *, name=None, value=None, inline=True):
|
||||||
|
"""Adds a field to the embed object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
name: str
|
||||||
|
The name of the field.
|
||||||
|
value: str
|
||||||
|
The value of the field.
|
||||||
|
inline: bool
|
||||||
|
Whether the field should be displayed inline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = {
|
||||||
|
'inline': inline
|
||||||
|
}
|
||||||
|
if name is not None:
|
||||||
|
field['name'] = name
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
field['value'] = value
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._fields.append(field)
|
||||||
|
except AttributeError:
|
||||||
|
self._fields = [field]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Converts this embed object into a dict."""
|
||||||
|
|
||||||
|
# add in the raw data into the dict
|
||||||
|
result = {
|
||||||
|
key[1:]: getattr(self, key)
|
||||||
|
for key in self.__slots__
|
||||||
|
if key[0] == '_' and hasattr(self, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
# deal with basic convenience wrappers
|
||||||
|
|
||||||
|
try:
|
||||||
|
colour = result.pop('colour')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
result['color'] = colour.value
|
||||||
|
|
||||||
|
try:
|
||||||
|
timestamp = result.pop('timestamp')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
result['timestamp'] = timestamp.isoformat()
|
||||||
|
|
||||||
|
# add in the non raw attribute ones
|
||||||
|
if self.type:
|
||||||
|
result['type'] = self.type
|
||||||
|
|
||||||
|
if self.description:
|
||||||
|
result['description'] = self.description
|
||||||
|
|
||||||
|
if self.url:
|
||||||
|
result['url'] = self.url
|
||||||
|
|
||||||
|
if self.title:
|
||||||
|
result['title'] = self.title
|
||||||
|
|
||||||
|
return result
|
@ -213,16 +213,21 @@ class HTTPClient:
|
|||||||
|
|
||||||
return self.post(self.ME + '/channels', json=payload, bucket=_func_())
|
return self.post(self.ME + '/channels', json=payload, bucket=_func_())
|
||||||
|
|
||||||
def send_message(self, channel_id, content, *, guild_id=None, tts=False):
|
def send_message(self, channel_id, content, *, guild_id=None, tts=False, embed=None):
|
||||||
url = '{0.CHANNELS}/{1}/messages'.format(self, channel_id)
|
url = '{0.CHANNELS}/{1}/messages'.format(self, channel_id)
|
||||||
payload = {
|
payload = {
|
||||||
'content': str(content),
|
|
||||||
'nonce': random_integer(-2**63, 2**63 - 1)
|
'nonce': random_integer(-2**63, 2**63 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if content:
|
||||||
|
payload['content'] = content
|
||||||
|
|
||||||
if tts:
|
if tts:
|
||||||
payload['tts'] = True
|
payload['tts'] = True
|
||||||
|
|
||||||
|
if embed:
|
||||||
|
payload['embed'] = embed
|
||||||
|
|
||||||
return self.post(url, json=payload, bucket='messages:' + str(guild_id))
|
return self.post(url, json=payload, bucket='messages:' + str(guild_id))
|
||||||
|
|
||||||
def send_typing(self, channel_id):
|
def send_typing(self, channel_id):
|
||||||
|
@ -643,6 +643,12 @@ Reaction
|
|||||||
.. autoclass:: Reaction
|
.. autoclass:: Reaction
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Embed
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: Embed
|
||||||
|
:members:
|
||||||
|
|
||||||
CallMessage
|
CallMessage
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user