Browse Source

Add support for multiple file attachments.

This is a breaking change. No longer does Messageable.send have a
filename keyword argument, instead this is all handled through the
discord.File model. To upload many files you must specify a list
of discord.File objects.
pull/530/head
Rapptz 8 years ago
parent
commit
bf2066278e
  1. 1
      discord/__init__.py
  2. 64
      discord/abc.py
  3. 73
      discord/file.py
  4. 9
      discord/http.py
  5. 6
      docs/api.rst

1
discord/__init__.py

@ -30,6 +30,7 @@ from .errors import *
from .calls import CallMessage, GroupCall
from .permissions import Permissions, PermissionOverwrite
from .role import Role
from .file import File
from .colour import Color, Colour
from .invite import Invite
from .object import Object

64
discord/abc.py

@ -37,6 +37,7 @@ from .errors import InvalidArgument
from .permissions import PermissionOverwrite, Permissions
from .role import Role
from .invite import Invite
from .file import File
from . import utils, compat
class _Undefined:
@ -536,7 +537,7 @@ class Messageable(metaclass=abc.ABCMeta):
raise NotImplementedError
@asyncio.coroutine
def send(self, content=None, *, tts=False, embed=None, file=None, filename=None, delete_after=None):
def send(self, content=None, *, tts=False, embed=None, file=None, files=None, delete_after=None):
"""|coro|
Sends a message to the destination with the content given.
@ -545,19 +546,10 @@ class Messageable(metaclass=abc.ABCMeta):
If the content is set to ``None`` (the default), then the ``embed`` parameter must
be provided.
The ``file`` parameter should be either a string denoting the location for a
file or a *file-like object*. The *file-like object* passed is **not closed**
at the end of execution. You are responsible for closing it yourself.
.. note::
If the file-like object passed is opened via ``open`` then the modes
'rb' should be used.
The ``filename`` parameter is the filename of the file.
If this is not given then it defaults to ``file.name`` or if ``file`` is a string
then the ``filename`` will default to the string given. You can overwrite
this value by passing this in.
To upload a single file, the ``file`` parameter should be used with a
single :class:`File` object. To upload multiple files, the ``files``
parameter should be used with a list of :class:`File` objects.
**Specifying both parameters will lead to an exception**.
If the ``embed`` parameter is provided, it must be of type :class:`Embed` and
it must be a rich embed type.
@ -570,12 +562,11 @@ class Messageable(metaclass=abc.ABCMeta):
Indicates if the message should be sent using text-to-speech.
embed: :class:`Embed`
The rich embed for the content.
file: file-like object or filename
The *file-like object* or file path to send.
filename: str
The filename of the file. Defaults to ``file.name`` if it's available.
If this is provided, you must also provide the ``file`` parameter or it
is silently ignored.
file: :class:`File`
The file to upload.
files: List[:class:`File`]
A list of files to upload. Must be a minimum of 2 and a
maximum of 10.
delete_after: float
If provided, the number of seconds to wait in the background
before deleting the message we just sent. If the deletion fails,
@ -587,6 +578,9 @@ class Messageable(metaclass=abc.ABCMeta):
Sending the message failed.
Forbidden
You do not have the proper permissions to send the message.
InvalidArgument
The ``files`` list is not of the appropriate size or
you specified both ``file`` and ``files``.
Returns
---------
@ -600,17 +594,29 @@ class Messageable(metaclass=abc.ABCMeta):
if embed is not None:
embed = embed.to_dict()
if file is not None and files is not None:
raise InvalidArgument('cannot pass both file and files parameter to send()')
if file is not None:
if not isinstance(file, File):
raise InvalidArgument('file parameter must be File')
try:
data = yield from state.http.send_files(channel.id, files=[(file.open_file(), file.filename)],
content=content, tts=tts, embed=embed)
finally:
file.close()
elif files is not None:
if len(files) < 2 or len(files) > 10:
raise InvalidArgument('files parameter must be a list of 2 to 10 elements')
try:
with open(file, 'rb') as f:
buffer = io.BytesIO(f.read())
if filename is None:
_, filename = os.path.split(file)
except TypeError:
buffer = file
data = yield from state.http.send_file(channel.id, buffer, filename=filename, content=content,
tts=tts, embed=embed)
param = [(f.open_file(), f.filename) for f in files]
data = yield from state.http.send_files(channel.id, files=param, content=content, tts=tts, embed=embed)
finally:
for f in files:
f.close()
else:
data = yield from state.http.send_message(channel.id, content, tts=tts, embed=embed)

73
discord/file.py

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2017 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 os.path
class File:
"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects.
Attributes
-----------
fp: Union[str, BinaryIO]
A file-like object opened in binary mode and read mode
or a filename representing a file in the hard drive to
open.
.. note::
If the file-like object passed is opened via ``open`` then the
modes 'rb' should be used.
To pass binary data, consider usage of ``io.BytesIO``.
filename: Optional[str]
The filename to display when uploading to Discord.
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
a string then the ``filename`` will default to the string given.
"""
__slots__ = ('fp', 'filename', '_true_fp')
def __init__(self, fp, filename=None):
self.fp = fp
self._true_fp = None
if filename is None:
if isinstance(fp, str):
_, self.filename = os.path.split(fp)
else:
self.filename = getattr(fp, 'name', None)
def open_file(self):
fp = self.fp
if isinstance(fp, str):
self._true_fp = fp = open(fp, 'rb')
return fp
def close(self):
if self._true_fp:
self._true_fp.close()

9
discord/http.py

@ -306,7 +306,7 @@ class HTTPClient:
def send_typing(self, channel_id):
return self.request(Route('POST', '/channels/{channel_id}/typing', channel_id=channel_id))
def send_file(self, channel_id, buffer, *, filename=None, content=None, tts=False, embed=None):
def send_files(self, channel_id, *, files, content=None, tts=False, embed=None):
r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id)
form = aiohttp.FormData()
@ -317,7 +317,12 @@ class HTTPClient:
payload['embed'] = embed
form.add_field('payload_json', utils.to_json(payload))
form.add_field('file', buffer, filename=filename, content_type='application/octet-stream')
if len(files) == 1:
fp = files[0]
form.add_field('file', fp[0], filename=fp[1], content_type='application/octet-stream')
else:
for index, (buffer, filename) in enumerate(files):
form.add_field('file%s' % index, buffer, filename=filename, content_type='application/octet-stream')
return self.request(r, data=form)

6
docs/api.rst

@ -827,6 +827,12 @@ Embed
.. autoclass:: Embed
:members:
File
~~~~~
.. autoclass:: File
:members:
CallMessage
~~~~~~~~~~~~

Loading…
Cancel
Save