add support for 3.5 api (#920)

* add support for 3.5 api

* removed "unused" import by accident

* Hardcoded values

Appearantly TG decided to change the size of a send image (again)

* test_official

* Improve coverage

* Finishing up

* spelling error

* pytest fixed tot < than 3.3 for python 3.3 support

* flake8

* rollback requirements

* as per CR

* object for provider_data

Make it possible to send an object that will be json-serialized for send_invoice + tests

* shorten error message

* using string_types
This commit is contained in:
Eldinnie 2017-12-08 22:38:59 +01:00 committed by GitHub
parent 1e22d570a3
commit 042d4bb2a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 434 additions and 16 deletions

View file

@ -0,0 +1,6 @@
telegram.InputMedia
===================
.. autoclass:: telegram.InputMedia
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
telegram.InputMediaPhoto
========================
.. autoclass:: telegram.InputMediaPhoto
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
telegram.InputMediaVideo
========================
.. autoclass:: telegram.InputMediaVideo
:members:
:show-inheritance:

View file

@ -21,6 +21,9 @@ telegram package
telegram.inlinekeyboardbutton
telegram.inlinekeyboardmarkup
telegram.inputfile
telegram.inputmedia
telegram.inputmediaphoto
telegram.inputmediavideo
telegram.keyboardbutton
telegram.location
telegram.message

View file

@ -96,6 +96,9 @@ from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOO
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 .version import __version__ # flake8: noqa
__author__ = 'devs@python-telegram-bot.org'
@ -121,5 +124,6 @@ __all__ = [
'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation',
'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption',
'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto',
'StickerSet', 'MaskPosition', 'CallbackGame'
'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto',
'InputMediaVideo'
]

View file

@ -21,10 +21,13 @@
"""This module contains an object that represents a Telegram Bot."""
import functools
import json
import logging
import warnings
from datetime import datetime
from future.utils import string_types
from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File,
ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet,
PhotoSize, Audio, Document, Sticker, Video, Voice, VideoNote, Location,
@ -335,7 +338,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
**kwargs):
"""Use this method to send photos.
@ -394,7 +397,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
**kwargs):
"""
Use this method to send audio files, if you want Telegram clients to display them in the
@ -465,7 +468,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
**kwargs):
"""Use this method to send general files.
@ -576,7 +579,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
width=None,
height=None,
**kwargs):
@ -646,7 +649,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
**kwargs):
"""
Use this method to send audio files, if you want Telegram clients to display the file
@ -708,7 +711,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
timeout=20.,
timeout=20,
**kwargs):
"""Use this method to send video messages.
@ -757,6 +760,51 @@ class Bot(TelegramObject):
return url, data
@log
def send_media_group(self,
chat_id,
media,
disable_notification=None,
reply_to_message_id=None,
timeout=20,
**kwargs):
"""Use this method to send a group of photos or videos as an album.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format @channelusername).
media (List[:class:`telegram.InputMedia`]): An array describing photos and videos to be
sent, must include 210 items.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
List[:class:`telegram.Message`]: An array of the sent Messages.
Raises:
:class:`telegram.TelegramError`
"""
# TODO: Make InputMediaPhoto, InputMediaVideo and send_media_group work with new files
url = '{0}/sendMediaGroup'.format(self.base_url)
media = [med.to_dict() for med in media]
data = {'chat_id': chat_id, 'media': media}
if reply_to_message_id:
data['reply_to_message_id'] = reply_to_message_id
if disable_notification:
data['disable_notification'] = disable_notification
result = self._request.post(url, data, timeout=timeout)
return [Message.de_json(res, self) for res in result]
@log
@message
def send_location(self,
@ -2153,6 +2201,7 @@ class Bot(TelegramObject):
disable_notification=False,
reply_to_message_id=None,
reply_markup=None,
provider_data=None,
timeout=None,
**kwargs):
"""Use this method to send invoices.
@ -2169,6 +2218,10 @@ class Bot(TelegramObject):
currency (:obj:`str`): Three-letter ISO 4217 currency code.
prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list of components
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.).
provider_data (:obj:`str` | :obj:`object`, optional): JSON-encoded data about the
invoice, which will be shared with the payment provider. A detailed description of
required fields should be provided by the payment provider. When an object is
passed, it will be encoded as JSON.
photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a
photo of the goods or a marketing image for a service. People like it better when
they see what they are paying for.
@ -2216,7 +2269,11 @@ class Bot(TelegramObject):
'currency': currency,
'prices': [p.to_dict() for p in prices]
}
if provider_data is not None:
if isinstance(provider_data, string_types):
data['provider_data'] = provider_data
else:
data['provider_data'] = json.dumps(provider_data)
if photo_url is not None:
data['photo_url'] = photo_url
if photo_size is not None:
@ -2965,6 +3022,7 @@ class Bot(TelegramObject):
sendVideo = send_video
sendVoice = send_voice
sendVideoNote = send_video_note
sendMediaGroup = send_media_group
sendLocation = send_location
editMessageLiveLocation = edit_message_live_location
stopMessageLiveLocation = stop_message_live_location

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram InputMedia Objects."""
from telegram import TelegramObject
class InputMedia(TelegramObject):
"""Base class for Telegram InputMedia Objects.
See :class:`telegram.InputMediaPhoto` and :class:`telegram.InputMediaVideo` for
detailed use.
"""
pass

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InputMediaPhoto."""
from telegram import InputMedia, PhotoSize
class InputMediaPhoto(InputMedia):
"""Represents a photo to be sent.
Attributes:
type (:obj:`str`): ``photo``.
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-200 characters.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-200 characters.
Note:
At the moment using a new file is not yet supported.
"""
# TODO: Make InputMediaPhoto, InputMediaVideo and send_media_group work with new files
def __init__(self, media, caption=None):
self.type = 'photo'
if isinstance(media, PhotoSize):
self.media = media.file_id
elif hasattr(media, 'read'):
raise ValueError(
'Sending files is not supported (yet). Use file_id, url or PhotoSize')
else:
self.media = media
if caption:
self.caption = caption

View file

@ -0,0 +1,74 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InputMediaPhoto."""
from telegram import InputMedia, Video
class InputMediaVideo(InputMedia):
"""Represents a video to be sent.
Attributes:
type (:obj:`str`): ``video``.
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Video` object to send.
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-200 characters.
width (:obj:`int`): Optional. Video width.
height (:obj:`int`): Optional. Video height.
duration (:obj:`int`): Optional. Video duration.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Video` object to send.
caption (:obj:`str`, optional): Caption of the video to be sent, 0-200 characters.
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration.
Note:
When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
width, height and duration from that video, unless otherwise specified with the optional
arguments.
At the moment using a new file is not yet supported.
"""
# TODO: Make InputMediaPhoto, InputMediaVideo and send_media_group work with new files
def __init__(self, media, caption=None, width=None, height=None, duration=None):
self.type = 'video'
if isinstance(media, Video):
self.media = media.file_id
self.width = media.width
self.height = media.height
self.duration = media.duration
elif hasattr(media, 'read'):
raise ValueError('Sending files is not supported (yet). Use file_id, url or Video')
else:
self.media = media
if caption:
self.caption = caption
if width:
self.width = width
if height:
self.height = height
if duration:
self.duration = duration

View file

@ -48,6 +48,8 @@ class Message(TelegramObject):
forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent.
reply_to_message (:class:`telegram.Message`): Optional. The original message.
edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited.
media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this
message belongs to.
text (:obj:`str`): Optional. The actual UTF-8 text of the message.
entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
usernames, URLs, bot commands, etc. that appear in the text. See
@ -117,6 +119,8 @@ class Message(TelegramObject):
``reply_to_message`` fields even if it itself is a reply.
edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix
time. Converted to :class:`datetime.datetime`.
media_group_id (:obj:`str`, optional): The unique identifier of a media message group this
message belongs to.
text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096
characters. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`.
entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special
@ -232,6 +236,7 @@ class Message(TelegramObject):
successful_payment=None,
forward_signature=None,
author_signature=None,
media_group_id=None,
bot=None,
**kwargs):
# Required
@ -277,6 +282,7 @@ class Message(TelegramObject):
self.successful_payment = successful_payment
self.forward_signature = forward_signature
self.author_signature = author_signature
self.media_group_id = media_group_id
self.bot = bot

View file

@ -104,8 +104,8 @@ class TestBot(object):
# Considering that the first message is old enough
bot.delete_message(chat_id=chat_id, message_id=1)
# send_photo, send_audio, send_document, send_sticker, send_video, send_voice
# and send_video_note are tested in their respective test modules. No need to duplicate here.
# send_photo, send_audio, send_document, send_sticker, send_video, send_voice, send_video_note
# and send_media_group are tested in their respective test modules. No need to duplicate here.
@flaky(3, 1)
@pytest.mark.timeout(10)

140
tests/test_inputmedia.py Normal file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from flaky import flaky
from telegram import InputMediaVideo, InputMediaPhoto, Message
from .test_video import video, video_file
from .test_photo import _photo, photo_file, photo, thumb
@pytest.fixture(scope='class')
def input_media_video():
return InputMediaVideo(media=TestInputMediaVideo.media,
caption=TestInputMediaVideo.caption,
width=TestInputMediaVideo.width,
height=TestInputMediaVideo.height,
duration=TestInputMediaVideo.duration)
@pytest.fixture(scope='class')
def input_media_photo():
return InputMediaPhoto(media=TestInputMediaPhoto.media,
caption=TestInputMediaPhoto.caption)
class TestInputMediaVideo(object):
type = "video"
media = "NOTAREALFILEID"
caption = "My Caption"
width = 3
height = 4
duration = 5
def test_expected_values(self, input_media_video):
assert input_media_video.type == self.type
assert input_media_video.media == self.media
assert input_media_video.caption == self.caption
assert input_media_video.width == self.width
assert input_media_video.height == self.height
assert input_media_video.duration == self.duration
def test_to_dict(self, input_media_video):
input_media_video_dict = input_media_video.to_dict()
assert input_media_video_dict['type'] == input_media_video.type
assert input_media_video_dict['media'] == input_media_video.media
assert input_media_video_dict['caption'] == input_media_video.caption
assert input_media_video_dict['width'] == input_media_video.width
assert input_media_video_dict['height'] == input_media_video.height
assert input_media_video_dict['duration'] == input_media_video.duration
def test_with_video(self, video):
# fixture found in test_video
input_media_video = InputMediaVideo(video, caption="test 3")
assert input_media_video.type == self.type
assert input_media_video.media == video.file_id
assert input_media_video.width == video.width
assert input_media_video.height == video.height
assert input_media_video.duration == video.duration
assert input_media_video.caption == "test 3"
def test_error_with_file(self, video_file):
# fixture found in test_video
with pytest.raises(ValueError, match="file_id, url or Video"):
InputMediaVideo(video_file)
class TestInputMediaPhoto(object):
type = "photo"
media = "NOTAREALFILEID"
caption = "My Caption"
def test_expected_values(self, input_media_photo):
assert input_media_photo.type == self.type
assert input_media_photo.media == self.media
assert input_media_photo.caption == self.caption
def test_to_dict(self, input_media_photo):
input_media_photo_dict = input_media_photo.to_dict()
assert input_media_photo_dict['type'] == input_media_photo.type
assert input_media_photo_dict['media'] == input_media_photo.media
assert input_media_photo_dict['caption'] == input_media_photo.caption
def test_with_photo(self, photo):
# fixture found in test_photo
imp = InputMediaPhoto(photo, caption="test 2")
assert imp.type == self.type
assert imp.media == photo.file_id
assert imp.caption == "test 2"
def test_error_with_file(self, photo_file):
# fixture found in test_photo
with pytest.raises(ValueError, match="file_id, url or PhotoSize"):
InputMediaPhoto(photo_file)
@pytest.fixture(scope='function')
def media_group(photo, thumb):
return [InputMediaPhoto(photo), InputMediaPhoto(thumb)]
class TestSendMediaGroup(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_media_group_photo(self, bot, chat_id, media_group):
messages = bot.send_media_group(chat_id, media_group)
assert isinstance(messages, list)
assert len(messages) == 2
assert all([isinstance(mes, Message) for mes in messages])
assert all([mes.media_group_id == messages[0].media_group_id for mes in messages])
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_media_group_all_args(self, bot, chat_id, media_group):
m1 = bot.send_message(chat_id, text="test")
messages = bot.send_media_group(chat_id, media_group, disable_notification=True,
reply_to_message_id=m1.message_id)
assert isinstance(messages, list)
assert len(messages) == 2
assert all([isinstance(mes, Message) for mes in messages])
assert all([mes.media_group_id == messages[0].media_group_id for mes in messages])
@pytest.mark.skip(reason="Needs a rework to send new files")
def test_send_media_group_new_files(self):
pass

View file

@ -32,6 +32,7 @@ def invoice():
class TestInvoice(object):
payload = 'payload'
prices = [LabeledPrice('Fish', 100), LabeledPrice('Fish Tax', 1000)]
provider_data = """{"test":"test"}"""
title = 'title'
description = 'description'
start_parameter = 'start_parameter'
@ -88,6 +89,7 @@ class TestInvoice(object):
self.start_parameter,
self.currency,
self.prices,
provider_data=self.provider_data,
photo_url='https://raw.githubusercontent.com/'
'python-telegram-bot/logos/master/'
'logo/png/ptb-logo_240.png',
@ -105,3 +107,24 @@ class TestInvoice(object):
assert message.invoice.description == self.description
assert message.invoice.title == self.title
assert message.invoice.total_amount == self.total_amount
def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token):
def test(_, url, data, **kwargs):
return data['provider_data'] == '{"test_data": 123456789}'
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.send_invoice(chat_id, self.title, self.description, self.payload,
provider_token, self.start_parameter, self.currency,
self.prices, provider_data={'test_data': 123456789})
def test_send_nonesense_as_provider_data(self, monkeypatch, bot, chat_id, provider_token):
def test(_, url, data, **kwargs):
return True
monkeypatch.setattr('telegram.utils.request.Request.post', test)
with pytest.raises(TypeError):
assert bot.send_invoice(chat_id, self.title, self.description, self.payload,
provider_token, self.start_parameter, self.currency,
self.prices, provider_data={'a', 'b', 'c'})

View file

@ -79,7 +79,10 @@ def message(bot):
'charge_id', 'provider_id',
order_info={})},
{'forward_signature': 'some_forward_sign'},
{'author_signature': 'some_author_sign'}
{'author_signature': 'some_author_sign'},
{'photo': [PhotoSize('photo_id', 50, 50)],
'caption': 'photo_file',
'media_group_id': 1234443322222}
],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'caption_entities', 'audio', 'document', 'game', 'photo', 'sticker', 'video',
@ -87,7 +90,7 @@ def message(bot):
'left_member', 'new_title', 'new_photo', 'delete_photo', 'group_created',
'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from',
'pinned', 'invoice', 'successful_payment', 'forward_signature',
'author_signature'])
'author_signature', 'photo_from_media_group'])
def message_params(bot, request):
return Message(message_id=TestMessage.id,
from_user=TestMessage.from_user,

View file

@ -105,7 +105,8 @@ def check_object(h4):
field = parameter[0]
if field == 'from':
field = 'from_user'
elif name.startswith('InlineQueryResult') and field == 'type':
elif ((name.startswith('InlineQueryResult') or
name.startswith('InputMedia')) and field == 'type'):
continue
elif field == 'remove_keyboard':
continue

View file

@ -174,9 +174,9 @@ class TestPhoto(object):
assert isinstance(photo.file_id, str)
assert photo.file_id != ''
assert isinstance(photo, PhotoSize)
assert photo.width == 1920
assert photo.height == 1080
assert photo.file_size == 30907
assert photo.width == 1280
assert photo.height == 720
assert photo.file_size == 33372
def test_send_with_photosize(self, monkeypatch, bot, chat_id, photo):
def test(_, url, data, **kwargs):