Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple inputfiles in requests #1184

Merged
merged 18 commits into from Aug 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 4 additions & 5 deletions telegram/__init__.py
Expand Up @@ -27,6 +27,7 @@
from .files.audio import Audio
from .files.voice import Voice
from .files.document import Document
from .files.animation import Animation
from .files.sticker import Sticker, StickerSet, MaskPosition
from .files.video import Video
from .files.contact import Contact
Expand All @@ -45,7 +46,6 @@
from .files.file import File
from .parsemode import ParseMode
from .messageentity import MessageEntity
from .games.animation import Animation
from .games.game import Game
from .games.callbackgame import CallbackGame
from .payment.shippingaddress import ShippingAddress
Expand Down Expand Up @@ -96,9 +96,8 @@
MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD,
MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND,
MAX_MESSAGES_PER_MINUTE_PER_GROUP)
from .files.inputmedia import InputMedia
from .files.inputmediavideo import InputMediaVideo
from .files.inputmediaphoto import InputMediaPhoto
from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation,
InputMediaAudio, InputMediaDocument)
from .version import __version__ # flake8: noqa

__author__ = 'devs@python-telegram-bot.org'
Expand All @@ -125,5 +124,5 @@
'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption',
'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto',
'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto',
'InputMediaVideo'
'InputMediaVideo', 'InputMediaAnimation', 'InputMediaAudio', 'InputMediaDocument'
]
267 changes: 205 additions & 62 deletions telegram/bot.py

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions telegram/chat.py
Expand Up @@ -273,6 +273,19 @@ def send_document(self, *args, **kwargs):
"""
return self.bot.send_document(self.id, *args, **kwargs)

def send_animation(self, *args, **kwargs):
"""Shortcut for::

bot.send_animation(Chat.id, *args, **kwargs)

Where Chat is the current instance.

Returns:
:class:`telegram.Message`: On success, instance representing the message posted.

"""
return self.bot.send_animation(self.id, *args, **kwargs)

def send_sticker(self, *args, **kwargs):
"""Shortcut for::

Expand Down
2 changes: 1 addition & 1 deletion telegram/ext/dispatcher.py
Expand Up @@ -143,7 +143,7 @@ def get_instance(cls):

"""
if cls.__singleton is not None:
return cls.__singleton()
return cls.__singleton() # pylint: disable=not-callable
else:
raise RuntimeError('{} not initialized or multiple instances exist'.format(
cls.__name__))
Expand Down
9 changes: 9 additions & 0 deletions telegram/ext/filters.py
Expand Up @@ -300,6 +300,15 @@ def filter(self, message):
document = _Document()
""":obj:`Filter`: Messages that contain :class:`telegram.Document`."""

class _Animation(BaseFilter):
name = 'Filters.animation'

def filter(self, message):
return bool(message.animation)

animation = _Animation()
""":obj:`Filter`: Messages that contain :class:`telegram.Animation`."""

class _Photo(BaseFilter):
name = 'Filters.photo'

Expand Down
16 changes: 15 additions & 1 deletion telegram/games/animation.py → telegram/files/animation.py
Expand Up @@ -26,6 +26,9 @@ class Animation(TelegramObject):

Attributes:
file_id (:obj:`str`): Unique file identifier.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined
by sender.
file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
Expand All @@ -34,6 +37,9 @@ class Animation(TelegramObject):

Args:
file_id (:obj:`str`): Unique file identifier.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender.
file_name (:obj:`str`, optional): Original animation filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
Expand All @@ -43,12 +49,20 @@ class Animation(TelegramObject):

def __init__(self,
file_id,
width,
height,
duration,
thumb=None,
file_name=None,
mime_type=None,
file_size=None,
**kwargs):
self.file_id = file_id
# Required
self.file_id = str(file_id)
self.width = int(width)
self.height = int(height)

self.duration = duration
self.thumb = thumb
self.file_name = file_name
self.mime_type = mime_type
Expand Down
10 changes: 9 additions & 1 deletion telegram/files/audio.py
Expand Up @@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Audio."""

from telegram import TelegramObject
from telegram import TelegramObject, PhotoSize


class Audio(TelegramObject):
Expand All @@ -32,6 +32,8 @@ class Audio(TelegramObject):
title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
file_size (:obj:`int`): Optional. File size.
thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to
which the music file belongs
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.

Args:
Expand All @@ -42,6 +44,8 @@ class Audio(TelegramObject):
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to
which the music file belongs
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.

Expand All @@ -54,6 +58,7 @@ def __init__(self,
title=None,
mime_type=None,
file_size=None,
thumb=None,
bot=None,
**kwargs):
# Required
Expand All @@ -64,6 +69,7 @@ def __init__(self,
self.title = title
self.mime_type = mime_type
self.file_size = file_size
self.thumb = thumb
self.bot = bot

self._id_attrs = (self.file_id,)
Expand All @@ -73,6 +79,8 @@ def de_json(cls, data, bot):
if not data:
return None

data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)

return cls(bot=bot, **data)

def get_file(self, timeout=None, **kwargs):
Expand Down
144 changes: 32 additions & 112 deletions telegram/files/inputfile.py
Expand Up @@ -19,22 +19,15 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InputFile."""

try:
# python 3
from email.generator import _make_boundary as choose_boundary
except ImportError:
# python 2
from mimetools import choose_boundary

import imghdr
import mimetypes
import os
import sys
from uuid import uuid4

from telegram import TelegramError

DEFAULT_MIME_TYPE = 'application/octet-stream'
USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)'
FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate',
'video_note', 'png_sticker')

Expand All @@ -53,98 +46,38 @@ class InputFile(object):

"""

def __init__(self, data):
self.data = data
self.boundary = choose_boundary()

for t in FILE_TYPES:
if t in data:
self.input_name = t
self.input_file = data.pop(t)
break
else:
raise TelegramError('Unknown inputfile type')

if hasattr(self.input_file, 'read'):
self.filename = None
self.input_file_content = self.input_file.read()
if 'filename' in data:
self.filename = self.data.pop('filename')
elif (hasattr(self.input_file, 'name') and
not isinstance(self.input_file.name, int) and # py3
self.input_file.name != '<fdopen>'): # py2
# on py2.7, pylint fails to understand this properly
# pylint: disable=E1101
self.filename = os.path.basename(self.input_file.name)

try:
self.mimetype = self.is_image(self.input_file_content)
if not self.filename or '.' not in self.filename:
self.filename = self.mimetype.replace('/', '.')
except TelegramError:
if self.filename:
self.mimetype = mimetypes.guess_type(
self.filename)[0] or DEFAULT_MIME_TYPE
else:
self.mimetype = DEFAULT_MIME_TYPE
def __init__(self, obj, filename=None, attach=None):
self.filename = None
self.input_file_content = obj.read()
self.attach = 'attached' + uuid4().hex if attach else None

if filename:
self.filename = filename
elif (hasattr(obj, 'name') and
not isinstance(obj.name, int) and # py3
obj.name != '<fdopen>'): # py2
# on py2.7, pylint fails to understand this properly
# pylint: disable=E1101
self.filename = os.path.basename(obj.name)

try:
self.mimetype = self.is_image(self.input_file_content)
except TelegramError:
if self.filename:
self.mimetype = mimetypes.guess_type(
self.filename)[0] or DEFAULT_MIME_TYPE
else:
self.mimetype = DEFAULT_MIME_TYPE
if not self.filename or '.' not in self.filename:
self.filename = self.mimetype.replace('/', '.')

if sys.version_info < (3,):
if isinstance(self.filename, unicode): # flake8: noqa pylint: disable=E0602
self.filename = self.filename.encode('utf-8', 'replace')

@property
def headers(self):
""":obj:`dict`: Headers."""

return {'User-agent': USER_AGENT, 'Content-type': self.content_type}

@property
def content_type(self):
""":obj:`str`: Content type"""
return 'multipart/form-data; boundary=%s' % self.boundary

def to_form(self):
"""Transform the inputfile to multipart/form data.

Returns:
:obj:`str`

"""
form = []
form_boundary = '--' + self.boundary

# Add data fields
for name in iter(self.data):
value = self.data[name]
form.extend([
form_boundary, 'Content-Disposition: form-data; name="%s"' % name, '', str(value)
])

# Add input_file to upload
form.extend([
form_boundary, 'Content-Disposition: form-data; name="%s"; filename="%s"' %
(self.input_name,
self.filename), 'Content-Type: %s' % self.mimetype, '', self.input_file_content
])

form.append('--' + self.boundary + '--')
form.append('')

return self._parse(form)

@staticmethod
def _parse(form):
if sys.version_info > (3,):
# on Python 3 form needs to be byte encoded
encoded_form = []
for item in form:
try:
encoded_form.append(item.encode())
except AttributeError:
encoded_form.append(item)

return b'\r\n'.join(encoded_form)
return '\r\n'.join(form)
def field_tuple(self):
return self.filename, self.input_file_content, self.mimetype

@staticmethod
def is_image(stream):
Expand All @@ -164,22 +97,9 @@ def is_image(stream):
raise TelegramError('Could not parse file content')

@staticmethod
def is_inputfile(data):
"""Check if the request is a file request.

Args:
data (Dict[:obj:`str`, :obj:`str`]): A dict of (str, str) key/value pairs.

Returns:
:obj:`bool`

"""
if data:
file_type = [i for i in iter(data) if i in FILE_TYPES]

if file_type:
file_content = data[file_type[0]]

return hasattr(file_content, 'read')
def is_file(obj):
return hasattr(obj, 'read')

return False
def to_dict(self):
if self.attach:
return 'attach://' + self.attach