aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2016-11-13 05:05:22 -0500
committerRapptz <[email protected]>2016-11-13 05:07:58 -0500
commitaf46718460d42cabe9c557719ef72e0df0742b54 (patch)
tree68764a275aae61e08d16ebe5a447d82a539dea26
parentVersion bump to v0.14.3 (diff)
downloaddiscord.py-af46718460d42cabe9c557719ef72e0df0742b54.tar.xz
discord.py-af46718460d42cabe9c557719ef72e0df0742b54.zip
Add support for rich embeds.
-rw-r--r--discord/__init__.py1
-rw-r--r--discord/client.py38
-rw-r--r--discord/embeds.py398
-rw-r--r--discord/http.py9
-rw-r--r--docs/api.rst6
5 files changed, 446 insertions, 6 deletions
diff --git a/discord/__init__.py b/discord/__init__.py
index 0b7ebba2..f2077a7e 100644
--- a/discord/__init__.py
+++ b/discord/__init__.py
@@ -37,6 +37,7 @@ from . import utils, opus, compat
from .voice_client import VoiceClient
from .enums import ChannelType, ServerRegion, Status, MessageType, VerificationLevel
from collections import namedtuple
+from .embeds import Embed
import logging
diff --git a/discord/client.py b/discord/client.py
index f9888fe7..f7d223bb 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -1043,7 +1043,7 @@ class Client:
return [User(**user) for user in data]
@asyncio.coroutine
- def send_message(self, destination, content, *, tts=False):
+ def send_message(self, destination, content=None, *, tts=False, embed=None):
"""|coro|
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`.
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
------------
destination
The location to send the message.
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
Indicates if the message should be sent using text-to-speech.
+ embed: :class:`Embed`
+ The rich embed for the content.
Raises
--------
@@ -1083,6 +1091,25 @@ class Client:
InvalidArgument
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
---------
:class:`Message`
@@ -1091,9 +1118,12 @@ class Client:
channel_id, guild_id = yield from self._resolve_destination(destination)
- content = str(content)
+ content = str(content) if content else None
+
+ 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)
+ 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'))
message = self.connection._create_message(channel=channel, **data)
return message
diff --git a/discord/embeds.py b/discord/embeds.py
new file mode 100644
index 00000000..d06ef9b9
--- /dev/null
+++ b/discord/embeds.py
@@ -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
diff --git a/discord/http.py b/discord/http.py
index d2cf2e4e..6379ade1 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -213,16 +213,21 @@ class HTTPClient:
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)
payload = {
- 'content': str(content),
'nonce': random_integer(-2**63, 2**63 - 1)
}
+ if content:
+ payload['content'] = content
+
if tts:
payload['tts'] = True
+ if embed:
+ payload['embed'] = embed
+
return self.post(url, json=payload, bucket='messages:' + str(guild_id))
def send_typing(self, channel_id):
diff --git a/docs/api.rst b/docs/api.rst
index 7df83e97..bd2cf40c 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -643,6 +643,12 @@ Reaction
.. autoclass:: Reaction
:members:
+Embed
+~~~~~~
+
+.. autoclass:: Embed
+ :members:
+
CallMessage
~~~~~~~~~~~~