diff --git a/.gitmodules b/.gitmodules index 8ee231a1c..ebe60816f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "telegram/vendor/urllib3"] - path = telegram/vendor/urllib3 + path = telegram/vendor/ptb_urllib3 url = https://github.com/python-telegram-bot/urllib3.git branch = ptb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17ce136fd..c1c987536 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,8 @@ hooks: - id: yapf files: ^(telegram|tests)/.*\.py$ + args: + - --diff - repo: git://github.com/pre-commit/pre-commit-hooks sha: 18d7035de5388cc7775be57f529c154bf541aab9 hooks: diff --git a/CHANGES.rst b/CHANGES.rst index a979e8411..f4b514252 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,40 @@ Changes ======= +**2017-05-29** + +*Released 6.0.2* + +- Avoid confusion with user's ``urllib3`` by renaming vendored ``urllib3`` to ``ptb_urllib3`` + +**2017-05-19** + +*Released 6.0.1* + +- Add support for ``User.language_code`` +- Fix ``Message.text_html`` and ``Message.text_markdown`` for messages with emoji + +**2017-05-19** + +*Released 6.0.0* + +- Add support for Bot API 2.3.1 +- Add support for ``deleteMessage`` API method +- New, simpler API for ``JobQueue`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/484 +- Download files into file-like objects - https://github.com/python-telegram-bot/python-telegram-bot/pull/459 +- Use vendor ``urllib3`` to address issues with timeouts + - The default timeout for messages is now 5 seconds. For sending media, the default timeout is now 20 seconds. +- String attributes that are not set are now ``None`` by default, instead of empty strings +- Add ``text_markdown`` and ``text_html`` properties to ``Message`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/507 +- Add support for Socks5 proxy - https://github.com/python-telegram-bot/python-telegram-bot/pull/518 +- Add support for filters in ``CommandHandler`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/536 +- Add the ability to invert (not) filters - https://github.com/python-telegram-bot/python-telegram-bot/pull/552 +- Add ``Filters.group`` and ``Filters.private`` +- Compatibility with GAE via ``urllib3.contrib`` package - https://github.com/python-telegram-bot/python-telegram-bot/pull/583 +- Add equality rich comparision operators to telegram objects - https://github.com/python-telegram-bot/python-telegram-bot/pull/604 +- Several bugfixes and other improvements +- Remove some deprecated code + **2017-04-17** *Released 5.3.1* @@ -52,6 +86,7 @@ Changes - Rework ``JobQueue`` - Introduce ``ConversationHandler`` +- Introduce ``telegram.constants`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/342 **2016-07-12** diff --git a/README.rst b/README.rst index 0ad57574f..8c7a8a6af 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,13 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -As of **4. Dec 2016**, all types and methods of the Telegram Bot API are supported. +As of **21. May 2017**, all types and methods of the Telegram Bot API 2.3.1 are supported. Additionally, the ``deleteMessage`` API function and the field ``User.language_code`` are supported. + +Also, version 6.1 beta 0 is available, offering full but experimental Bot API 3.0 coverage: + +.. code:: shell + + $ pip install python-telegram-bot==6.1b0 ========== Installing diff --git a/docs/source/conf.py b/docs/source/conf.py index 13f1adb34..d80714c0b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,9 +59,9 @@ author = u'Leandro Toledo' # built documents. # # The short X.Y version. -version = '5.3' # telegram.__version__[:3] +version = '6.0' # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. -release = '5.3.1' # telegram.__version__ +release = '6.0.2' # telegram.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/examples/conversationbot.py b/examples/conversationbot.py index 96a25a6d9..6dcff8643 100644 --- a/examples/conversationbot.py +++ b/examples/conversationbot.py @@ -56,7 +56,7 @@ def gender(bot, update): def photo(bot, update): user = update.message.from_user - photo_file = bot.getFile(update.message.photo[-1].file_id) + photo_file = bot.get_file(update.message.photo[-1].file_id) photo_file.download('user_photo.jpg') logger.info("Photo of %s: %s" % (user.first_name, 'user_photo.jpg')) update.message.reply_text('Gorgeous! Now, send me your location please, ' diff --git a/examples/echobot.py b/examples/echobot.py index afe532060..b7e6e0a4a 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -20,7 +20,7 @@ def main(): # get the first pending update_id, this is so we can skip over it in case # we get an "Unauthorized" exception. try: - update_id = bot.getUpdates()[0].update_id + update_id = bot.get_updates()[0].update_id except IndexError: update_id = None @@ -39,9 +39,7 @@ def main(): def echo(bot): global update_id # Request updates after the last update_id - for update in bot.getUpdates(offset=update_id, timeout=10): - # chat_id is required to reply to any message - chat_id = update.message.chat_id + for update in bot.get_updates(offset=update_id, timeout=10): update_id = update.update_id + 1 if update.message: # your bot can receive updates without messages diff --git a/examples/inlinebot.py b/examples/inlinebot.py index 2db044d10..457ef5bc1 100644 --- a/examples/inlinebot.py +++ b/examples/inlinebot.py @@ -72,7 +72,7 @@ def inlinequery(bot, update): def error(bot, update, error): - logger.warn('Update "%s" caused error "%s"' % (update, error)) + logger.warning('Update "%s" caused error "%s"' % (update, error)) def main(): diff --git a/examples/inlinekeyboard.py b/examples/inlinekeyboard.py index 326581a9f..4966702b0 100644 --- a/examples/inlinekeyboard.py +++ b/examples/inlinekeyboard.py @@ -26,9 +26,9 @@ def start(bot, update): def button(bot, update): query = update.callback_query - bot.editMessageText(text="Selected option: %s" % query.data, - chat_id=query.message.chat_id, - message_id=query.message.message_id) + bot.edit_message_text(text="Selected option: %s" % query.data, + chat_id=query.message.chat_id, + message_id=query.message.message_id) def help(bot, update): diff --git a/examples/timerbot.py b/examples/timerbot.py index 6c54fb5a0..2a7d7cbe2 100644 --- a/examples/timerbot.py +++ b/examples/timerbot.py @@ -35,7 +35,7 @@ def start(bot, update): def alarm(bot, job): """Function to send the alarm message""" - bot.sendMessage(job.context, text='Beep!') + bot.send_message(job.context, text='Beep!') def set(bot, update, args, job_queue, chat_data): @@ -49,9 +49,8 @@ def set(bot, update, args, job_queue, chat_data): return # Add job to queue - job = Job(alarm, due, repeat=False, context=chat_id) + job = job_queue.run_once(alarm, due, context=chat_id) chat_data['job'] = job - job_queue.put(job) update.message.reply_text('Timer successfully set!') diff --git a/setup.py b/setup.py index 44c6440ab..6b8ee3e1f 100644 --- a/setup.py +++ b/setup.py @@ -18,11 +18,6 @@ def requirements(): packages = find_packages(exclude=['tests*']) -packages.extend(['telegram.vendor.urllib3.urllib3', - 'telegram.vendor.urllib3.urllib3.packages', 'telegram.vendor.urllib3.urllib3.packages.ssl_match_hostname', - 'telegram.vendor.urllib3.urllib3.packages.backports', 'telegram.vendor.urllib3.urllib3.contrib', - 'telegram.vendor.urllib3.urllib3.util', - ]) with codecs.open('README.rst', 'r', 'utf-8') as fd: fn = os.path.join('telegram', 'version.py') diff --git a/telegram/__init__.py b/telegram/__init__.py index 3ef6cda08..388cd851a 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -22,8 +22,6 @@ from sys import version_info import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'vendor', 'urllib3')) - from .base import TelegramObject from .user import User from .chat import Chat @@ -94,6 +92,7 @@ from .precheckoutquery import PreCheckoutQuery from .shippingquery import ShippingQuery from .webhookinfo import WebhookInfo from .gamehighscore import GameHighScore +from .videonote import VideoNote from .update import Update from .bot import Bot from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS, @@ -123,6 +122,6 @@ __all__ = [ 'Video', 'Voice', 'MAX_MESSAGE_LENGTH', 'MAX_CAPTION_LENGTH', 'SUPPORTED_WEBHOOK_PORTS', 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT', 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation', - 'Game', 'GameHighScore', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', + 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery' ] diff --git a/telegram/bot.py b/telegram/bot.py index 35a022fee..7462c3e1a 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -671,6 +671,66 @@ class Bot(TelegramObject): timeout=timeout, **kwargs) + @log + def send_video_note(self, + chat_id, + video_note, + duration=None, + length=None, + disable_notification=False, + reply_to_message_id=None, + reply_markup=None, + timeout=20., + **kwargs): + """As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute + long. Use this method to send video messages + + Args: + chat_id (int|str): Unique identifier for the message recipient - Chat id. + video_note (InputFile|str): Video note to send. Pass a file_id as String to send a + video note that exists on the Telegram servers (recommended) or upload a new video. + Sending video notes by a URL is currently unsupported + duration (Optional[int]): Duration of sent audio in seconds. + length (Optional[int]): Video width and height + disable_notification (Optional[bool]): Sends the message silently. iOS users will not + receive a notification, Android users will receive a notification with no sound. + reply_to_message_id (Optional[int]): If the message is a reply, ID of the original + message. + reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (Optional[int|float]): Send file timeout (default: 20 seconds). + **kwargs (dict): Arbitrary keyword arguments. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{0}/sendVideoNote'.format(self.base_url) + + data = {'chat_id': chat_id, 'video_note': video_note} + + if duration is not None: + data['duration'] = duration + if length is not None: + data['length'] = length + + return self._message_wrapper( + url, + data, + chat_id=chat_id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + **kwargs) + @log @message def send_location(self, @@ -1963,6 +2023,7 @@ class Bot(TelegramObject): sendSticker = send_sticker sendVideo = send_video sendVoice = send_voice + sendVideoNote = send_video_note sendLocation = send_location sendVenue = send_venue sendContact = send_contact diff --git a/telegram/chataction.py b/telegram/chataction.py index 99b39139f..df6e99a9e 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -31,3 +31,5 @@ class ChatAction(object): UPLOAD_AUDIO = 'upload_audio' UPLOAD_DOCUMENT = 'upload_document' FIND_LOCATION = 'find_location' + RECORD_VIDEO_NOTE = 'record_video_note' + UPLOAD_VIDEO_NOTE = 'upload_video_note' diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 62bf26e5c..1942fa859 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -19,6 +19,11 @@ """ This module contains the Filters for use with the MessageHandler class """ from telegram import Chat +try: + str_type = base_string +except NameError: + str_type = str + class BaseFilter(object): """Base class for all Message Filters @@ -209,13 +214,68 @@ class Filters(object): class _StatusUpdate(BaseFilter): + class _NewChatMembers(BaseFilter): + + def filter(self, message): + return bool(message.new_chat_members) + + new_chat_members = _NewChatMembers() + + class _LeftChatMember(BaseFilter): + + def filter(self, message): + return bool(message.left_chat_member) + + left_chat_member = _LeftChatMember() + + class _NewChatTitle(BaseFilter): + + def filter(self, message): + return bool(message.new_chat_title) + + new_chat_title = _NewChatTitle() + + class _NewChatPhoto(BaseFilter): + + def filter(self, message): + return bool(message.new_chat_photo) + + new_chat_photo = _NewChatPhoto() + + class _DeleteChatPhoto(BaseFilter): + + def filter(self, message): + return bool(message.delete_chat_photo) + + delete_chat_photo = _DeleteChatPhoto() + + class _ChatCreated(BaseFilter): + + def filter(self, message): + return bool(message.group_chat_created or message.supergroup_chat_created or + message.channel_chat_created) + + chat_created = _ChatCreated() + + class _Migrate(BaseFilter): + + def filter(self, message): + return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) + + migrate = _Migrate() + + class _PinnedMessage(BaseFilter): + + def filter(self, message): + return bool(message.pinned_message) + + pinned_message = _PinnedMessage() + def filter(self, message): - return bool(message.new_chat_member or message.left_chat_member - or message.new_chat_title or message.new_chat_photo - or message.delete_chat_photo or message.group_chat_created - or message.supergroup_chat_created or message.channel_chat_created - or message.migrate_to_chat_id or message.migrate_from_chat_id - or message.pinned_message) + return bool(self.new_chat_members(message) or self.left_chat_member(message) or + self.new_chat_title(message) or self.new_chat_photo(message) or + self.delete_chat_photo(message) or self.chat_created(message) or + self.migrate(message) or self.pinned_message(message)) status_update = _StatusUpdate() @@ -277,3 +337,24 @@ class Filters(object): return bool(message.successful_payment) successful_payment = _SuccessfulPayment() + + class language(BaseFilter): + """ + Filters messages to only allow those which are from users with a certain language code. + Note that according to telegrams documentation, every single user does not have the + language_code attribute. + + Args: + lang (str|list): Which language code(s) to allow through. This will be matched using + .startswith meaning that 'en' will match both 'en_US' and 'en_GB' + """ + + def __init__(self, lang): + if isinstance(lang, str_type): + self.lang = [lang] + else: + self.lang = lang + + def filter(self, message): + return message.from_user.language_code and any( + [message.from_user.language_code.startswith(x) for x in self.lang]) diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index f19778da4..347d29ee8 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -37,8 +37,6 @@ class MessageHandler(Handler): callback (function): A function that takes ``bot, update`` as positional arguments. It will be called when the ``check_update`` has determined that an update should be processed by this handler. - allow_edited (Optional[bool]): If the handler should also accept edited messages. - Default is ``False`` pass_update_queue (optional[bool]): If the handler should be passed the update queue as a keyword argument called ``update_queue``. It can be used to insert updates. Default is ``False`` @@ -52,8 +50,12 @@ class MessageHandler(Handler): For each update in the same chat, it will be the same ``dict``. Default is ``False``. message_updates (Optional[bool]): Should "normal" message updates be handled? Default is ``True``. + allow_edited (Optional[bool]): If the handler should also accept edited messages. + Default is ``False`` - Deprecated. use edited updates instead. channel_post_updates (Optional[bool]): Should channel posts updates be handled? Default is ``True``. + edited_updates (Optional[bool]): Should "edited" message updates be handled? Default is + ``False``. """ @@ -66,9 +68,14 @@ class MessageHandler(Handler): pass_user_data=False, pass_chat_data=False, message_updates=True, - channel_post_updates=True): - if not message_updates and not channel_post_updates: - raise ValueError('Both message_updates & channel_post_updates are False') + channel_post_updates=True, + edited_updates=False): + if not message_updates and not channel_post_updates and not edited_updates: + raise ValueError( + 'message_updates, channel_post_updates and edited_updates are all False') + if allow_edited: + warnings.warn('allow_edited is getting deprecated, please use edited_updates instead') + edited_updates = allow_edited super(MessageHandler, self).__init__( callback, @@ -77,9 +84,9 @@ class MessageHandler(Handler): pass_user_data=pass_user_data, pass_chat_data=pass_chat_data) self.filters = filters - self.allow_edited = allow_edited self.message_updates = message_updates self.channel_post_updates = channel_post_updates + self.edited_updates = edited_updates # We put this up here instead of with the rest of checking code # in check_update since we don't wanna spam a ton @@ -88,17 +95,13 @@ class MessageHandler(Handler): 'deprecated, please use bitwise operators (& and |) ' 'instead. More info: https://git.io/vPTbc.') - def _is_allowed_message(self, update): - return (self.message_updates - and (update.message or (update.edited_message and self.allow_edited))) - - def _is_allowed_channel_post(self, update): - return (self.channel_post_updates - and (update.channel_post or (update.edited_channel_post and self.allow_edited))) + def _is_allowed_update(self, update): + return any([(self.message_updates and update.message), + (self.edited_updates and update.edited_message), + (self.channel_post_updates and update.channel_post)]) def check_update(self, update): - if (isinstance(update, Update) - and (self._is_allowed_message(update) or self._is_allowed_channel_post(update))): + if isinstance(update, Update) and self._is_allowed_update(update): if not self.filters: res = True diff --git a/telegram/inlinequeryresultgif.py b/telegram/inlinequeryresultgif.py index e5f8961a7..427986b2a 100644 --- a/telegram/inlinequeryresultgif.py +++ b/telegram/inlinequeryresultgif.py @@ -32,6 +32,7 @@ class InlineQueryResultGif(InlineQueryResult): thumb_url (str): URL of the static thumbnail for the result (jpeg or gif). gif_width (Optional[int]): Width of the GIF. gif_height (Optional[int]): Height of the GIF. + gif_duration (Optional[int]): Duration of the GIF. title (Optional[str]): Title for the result. caption (Optional[str]): Caption of the GIF file to be sent, 0-200 characters. reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): Inline keyboard attached @@ -45,6 +46,7 @@ class InlineQueryResultGif(InlineQueryResult): thumb_url (str): gif_width (Optional[int]): gif_height (Optional[int]): + gif_duration (Optional[int]): title (Optional[str]): caption (Optional[str]): reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): @@ -63,6 +65,7 @@ class InlineQueryResultGif(InlineQueryResult): caption=None, reply_markup=None, input_message_content=None, + gif_duration=None, **kwargs): # Required @@ -75,6 +78,8 @@ class InlineQueryResultGif(InlineQueryResult): self.gif_width = gif_width if gif_height: self.gif_height = gif_height + if gif_duration: + self.gif_duration = gif_duration if title: self.title = title if caption: diff --git a/telegram/inlinequeryresultmpeg4gif.py b/telegram/inlinequeryresultmpeg4gif.py index 3656a7b37..228079d10 100644 --- a/telegram/inlinequeryresultmpeg4gif.py +++ b/telegram/inlinequeryresultmpeg4gif.py @@ -32,6 +32,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): thumb_url (str): URL of the static thumbnail (jpeg or gif) for the result. mpeg4_width (Optional[int]): Video width. mpeg4_height (Optional[int]): Video height. + mpeg4_duration (Optional[int]): Video duration title (Optional[str]): Title for the result. caption (Optional[str]): Caption of the MPEG-4 file to be sent, 0-200 characters. reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): Inline keyboard attached @@ -44,6 +45,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): thumb_url (str): URL of the static thumbnail (jpeg or gif) for the result. mpeg4_width (Optional[int]): Video width. mpeg4_height (Optional[int]): Video height. + mpeg4_duration (Optional[int]): Video duration title (Optional[str]): Title for the result. caption (Optional[str]): Caption of the MPEG-4 file to be sent, 0-200 characters. reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): Inline keyboard attached @@ -64,6 +66,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): caption=None, reply_markup=None, input_message_content=None, + mpeg4_duration=None, **kwargs): # Required @@ -76,6 +79,8 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): self.mpeg4_width = mpeg4_width if mpeg4_height: self.mpeg4_height = mpeg4_height + if mpeg4_duration: + self.mpeg4_duration = mpeg4_duration if title: self.title = title if caption: diff --git a/telegram/inputfile.py b/telegram/inputfile.py index e73115f79..2f19b6d69 100644 --- a/telegram/inputfile.py +++ b/telegram/inputfile.py @@ -35,7 +35,8 @@ 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') +FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate', + 'video_note') class InputFile(object): @@ -45,27 +46,11 @@ class InputFile(object): self.data = data self.boundary = choose_boundary() - if 'audio' in data: - self.input_name = 'audio' - self.input_file = data.pop('audio') - elif 'document' in data: - self.input_name = 'document' - self.input_file = data.pop('document') - elif 'photo' in data: - self.input_name = 'photo' - self.input_file = data.pop('photo') - elif 'sticker' in data: - self.input_name = 'sticker' - self.input_file = data.pop('sticker') - elif 'video' in data: - self.input_name = 'video' - self.input_file = data.pop('video') - elif 'voice' in data: - self.input_name = 'voice' - self.input_file = data.pop('voice') - elif 'certificate' in data: - self.input_name = 'certificate' - self.input_file = data.pop('certificate') + 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') diff --git a/telegram/message.py b/telegram/message.py index dfab2686a..96217447a 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -24,7 +24,9 @@ from time import mktime from telegram import (Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice, SuccessfulPayment) +from telegram.utils.deprecate import warn_deprecate_obj from telegram.utils.helpers import escape_html, escape_markdown +from telegram.videonote import VideoNote class Message(TelegramObject): @@ -54,6 +56,8 @@ class Message(TelegramObject): entities (List[:class:`telegram.MessageEntity`]): For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text. See parse_entity and parse_entities methods for how to use properly + video_note (:class:`telegram.VideoNote`): Message is a video note, information about the + video message audio (:class:`telegram.Audio`): Message is an audio file, information about the file document (:class:`telegram.Document`): Message is a general file, information about the file @@ -128,6 +132,7 @@ class Message(TelegramObject): location=None, venue=None, new_chat_member=None, + new_chat_members=None, left_chat_member=None, new_chat_title=None, new_chat_photo=None, @@ -142,6 +147,7 @@ class Message(TelegramObject): invoice=None, successful_payment=None, bot=None, + video_note=None, **kwargs): # Required self.message_id = int(message_id) @@ -163,11 +169,13 @@ class Message(TelegramObject): self.sticker = sticker self.video = video self.voice = voice + self.video_note = video_note self.caption = caption self.contact = contact self.location = location self.venue = venue - self.new_chat_member = new_chat_member + self._new_chat_member = new_chat_member + self.new_chat_members = new_chat_members self.left_chat_member = left_chat_member self.new_chat_title = new_chat_title self.new_chat_photo = new_chat_photo @@ -222,10 +230,12 @@ class Message(TelegramObject): data['sticker'] = Sticker.de_json(data.get('sticker'), bot) data['video'] = Video.de_json(data.get('video'), bot) data['voice'] = Voice.de_json(data.get('voice'), bot) + data['video_note'] = VideoNote.de_json(data.get('video_note'), bot) data['contact'] = Contact.de_json(data.get('contact'), bot) data['location'] = Location.de_json(data.get('location'), bot) data['venue'] = Venue.de_json(data.get('venue'), bot) data['new_chat_member'] = User.de_json(data.get('new_chat_member'), bot) + data['new_chat_members'] = User.de_list(data.get('new_chat_members'), bot) data['left_chat_member'] = User.de_json(data.get('left_chat_member'), bot) data['new_chat_photo'] = PhotoSize.de_list(data.get('new_chat_photo'), bot) data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) @@ -261,6 +271,9 @@ class Message(TelegramObject): data['entities'] = [e.to_dict() for e in self.entities] if self.new_chat_photo: data['new_chat_photo'] = [p.to_dict() for p in self.new_chat_photo] + data['new_chat_member'] = data.pop('_new_chat_member', None) + if self.new_chat_members: + data['new_chat_members'] = [u.to_dict() for u in self.new_chat_members] return data @@ -412,6 +425,23 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.sendVideo(self.chat_id, *args, **kwargs) + def reply_video_note(self, *args, **kwargs): + """ + Shortcut for ``bot.send_video_note(update.message.chat_id, *args, **kwargs)`` + + Keyword Args: + quote (Optional[bool]): If set to ``True``, the video is sent as an actual reply to + this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter + will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + + self._quote(kwargs) + return self.bot.send_video_note(self.chat_id, *args, **kwargs) + def reply_voice(self, *args, **kwargs): """ Shortcut for ``bot.sendVoice(update.message.chat_id, *args, **kwargs)`` @@ -632,6 +662,9 @@ class Message(TelegramObject): """ entities = self.parse_entities() message_text = self.text + if not sys.maxunicode == 0xffff: + message_text = message_text.encode('utf-16-le') + markdown_text = '' last_offset = 0 @@ -651,10 +684,18 @@ class Message(TelegramObject): else: insert = text - markdown_text += escape_html(message_text[last_offset:entity.offset]) + insert + if sys.maxunicode == 0xffff: + markdown_text += escape_html(message_text[last_offset:entity.offset]) + insert + else: + markdown_text += escape_html(message_text[last_offset * 2:entity.offset * 2] + .decode('utf-16-le')) + insert + last_offset = entity.offset + entity.length - markdown_text += message_text[last_offset:] + if sys.maxunicode == 0xffff: + markdown_text += escape_html(message_text[last_offset:]) + else: + markdown_text += escape_html(message_text[last_offset * 2:].decode('utf-16-le')) return markdown_text @property @@ -671,6 +712,9 @@ class Message(TelegramObject): """ entities = self.parse_entities() message_text = self.text + if not sys.maxunicode == 0xffff: + message_text = message_text.encode('utf-16-le') + markdown_text = '' last_offset = 0 @@ -689,9 +733,21 @@ class Message(TelegramObject): insert = '```' + text + '```' else: insert = text + if sys.maxunicode == 0xffff: + markdown_text += escape_markdown(message_text[last_offset:entity.offset]) + insert + else: + markdown_text += escape_markdown(message_text[last_offset * 2:entity.offset * 2] + .decode('utf-16-le')) + insert - markdown_text += escape_markdown(message_text[last_offset:entity.offset]) + insert last_offset = entity.offset + entity.length - markdown_text += message_text[last_offset:] + if sys.maxunicode == 0xffff: + markdown_text += escape_markdown(message_text[last_offset:]) + else: + markdown_text += escape_markdown(message_text[last_offset * 2:].decode('utf-16-le')) return markdown_text + + @property + def new_chat_member(self): + warn_deprecate_obj('new_chat_member', 'new_chat_members') + return self._new_chat_member diff --git a/telegram/user.py b/telegram/user.py index 73f27b97d..e269ae3c7 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -26,21 +26,23 @@ class User(TelegramObject): """This object represents a Telegram User. Attributes: - id (int): - first_name (str): - last_name (str): - username (str): - type (str): + id (int): Unique identifier for this user or bot + first_name (str): User's or bot's first name + last_name (str): User's or bot's last name + username (str): User's or bot's username + language_code (str): IETF language tag of the user's language + type (str): Deprecated Args: - id (int): - first_name (str): + id (int): Unique identifier for this user or bot + first_name (str): User's or bot's first name **kwargs: Arbitrary keyword arguments. Keyword Args: - type (Optional[str]): - last_name (Optional[str]): - username (Optional[str]): + type (Optional[str]): Deprecated + last_name (Optional[str]): User's or bot's last name + username (Optional[str]): User's or bot's username + language_code (Optional[str]): IETF language tag of the user's language bot (Optional[Bot]): The Bot to use for instance methods """ @@ -50,6 +52,7 @@ class User(TelegramObject): type=None, last_name=None, username=None, + language_code=None, bot=None, **kwargs): # Required @@ -59,6 +62,7 @@ class User(TelegramObject): self.type = type self.last_name = last_name self.username = username + self.language_code = language_code self.bot = bot @@ -95,3 +99,22 @@ class User(TelegramObject): Shortcut for ``bot.getUserProfilePhotos(update.message.from_user.id, *args, **kwargs)`` """ return self.bot.getUserProfilePhotos(self.id, *args, **kwargs) + + @staticmethod + def de_list(data, bot): + """ + Args: + data (list): + bot (telegram.Bot): + + Returns: + List: + """ + if not data: + return [] + + users = list() + for user in data: + users.append(User.de_json(user, bot)) + + return users diff --git a/telegram/utils/deprecate.py b/telegram/utils/deprecate.py index af06e1b02..5cbec860e 100644 --- a/telegram/utils/deprecate.py +++ b/telegram/utils/deprecate.py @@ -21,8 +21,18 @@ import warnings -def warn_deprecate_obj(old, new): - warnings.warn('{0} is being deprecated, please use {1} from now on'.format(old, new)) +# We use our own DeprecationWarning since they are muted by default and "UserWarning" makes it +# seem like it's the user that issued the warning +# We name it something else so that you don't get confused when you attempt to suppress it +class TelegramDeprecationWarning(Warning): + pass + + +def warn_deprecate_obj(old, new, stacklevel=3): + warnings.warn( + '{0} is being deprecated, please use {1} from now on.'.format(old, new), + category=TelegramDeprecationWarning, + stacklevel=stacklevel) def deprecate(func, old, new): diff --git a/telegram/utils/request.py b/telegram/utils/request.py index 14c114334..19531f528 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -20,6 +20,7 @@ import os import socket import logging +import warnings try: import ujson as json @@ -27,10 +28,15 @@ except ImportError: import json import certifi -import urllib3 -import urllib3.contrib.appengine -from urllib3.connection import HTTPConnection -from urllib3.util.timeout import Timeout +try: + import telegram.vendor.ptb_urllib3.urllib3 as urllib3 + import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine + from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection + from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout +except ImportError: + warnings.warn("python-telegram-bot wasn't properly installed. Please refer to README.rst on " + "how to properly install.") + raise from telegram import (InputFile, TelegramError) from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated, @@ -90,16 +96,16 @@ class Request(object): proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') if not proxy_url: - if urllib3.contrib.appengine.is_appengine_sandbox(): + if appengine.is_appengine_sandbox(): # Use URLFetch service if running in App Engine - mgr = urllib3.contrib.appengine.AppEngineManager() + mgr = appengine.AppEngineManager() else: mgr = urllib3.PoolManager(**kwargs) else: kwargs.update(urllib3_proxy_kwargs) if proxy_url.startswith('socks'): try: - from urllib3.contrib.socks import SOCKSProxyManager + from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager except ImportError: raise RuntimeError('PySocks is missing') mgr = SOCKSProxyManager(proxy_url, **kwargs) diff --git a/telegram/vendor/__init__.py b/telegram/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/telegram/vendor/ptb_urllib3 b/telegram/vendor/ptb_urllib3 new file mode 160000 index 000000000..ddb495340 --- /dev/null +++ b/telegram/vendor/ptb_urllib3 @@ -0,0 +1 @@ +Subproject commit ddb495340152a4e51800226dc68a479e48c055ff diff --git a/telegram/vendor/urllib3 b/telegram/vendor/urllib3 deleted file mode 160000 index 4b076eedf..000000000 --- a/telegram/vendor/urllib3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4b076eedffc1afabf0215ced3820603de73d1ce7 diff --git a/telegram/version.py b/telegram/version.py index 508ad5e16..5bf5f688e 100644 --- a/telegram/version.py +++ b/telegram/version.py @@ -17,4 +17,4 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -__version__ = '5.3.1' +__version__ = '6.0.2' diff --git a/telegram/videonote.py b/telegram/videonote.py new file mode 100644 index 000000000..ba3616d43 --- /dev/null +++ b/telegram/videonote.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2017 +# Leandro Toledo de Souza +# +# 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 VideoNote.""" + +from telegram import PhotoSize, TelegramObject + + +class VideoNote(TelegramObject): + """This object represents a Telegram VideoNote. + + Attributes: + file_id (str): Unique identifier for this file + length (int): Video width and height as defined by sender + duration (int): Duration of the video in seconds as defined by sender + thumb (Optional[:class:`telegram.PhotoSize`]): Video thumbnail + file_size (Optional[int]): File size + """ + + def __init__(self, file_id, length, duration, thumb=None, file_size=None, **kwargs): + # Required + self.file_id = str(file_id) + self.length = int(length) + self.duration = int(duration) + # Optionals + self.thumb = thumb + self.file_size = file_size + + self._id_attrs = (self.file_id,) + + @staticmethod + def de_json(data, bot): + """ + Args: + data (dict): + bot (telegram.Bot): + + Returns: + telegram.VideoNote: + """ + if not data: + return None + + data = super(VideoNote, VideoNote).de_json(data, bot) + + data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) + + return VideoNote(**data) diff --git a/tests/test_bot.py b/tests/test_bot.py index a3ddb5108..700253a61 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -317,7 +317,7 @@ class BotTest(BaseTest, unittest.TestCase): self.assertEqual(text, fwdmsg.text) self.assertEqual(fwdmsg.forward_from_message_id, msg.message_id) - @flaky(3, 1) + @flaky(20, 1) @timeout(10) def test_set_webhook_get_webhook_info(self): url = 'https://python-telegram-bot.org/test/webhook' diff --git a/tests/test_filters.py b/tests/test_filters.py index 2ed4b7ebe..02d667669 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -121,48 +121,59 @@ class FiltersTest(BaseTest, unittest.TestCase): def test_filters_status_update(self): self.assertFalse(Filters.status_update(self.message)) - self.message.new_chat_member = 'test' + self.message.new_chat_members = ['test'] self.assertTrue(Filters.status_update(self.message)) - self.message.new_chat_member = None + self.assertTrue(Filters.status_update.new_chat_members(self.message)) + self.message.new_chat_members = None self.message.left_chat_member = 'test' self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.left_chat_member(self.message)) self.message.left_chat_member = None self.message.new_chat_title = 'test' self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.new_chat_title(self.message)) self.message.new_chat_title = '' self.message.new_chat_photo = 'test' self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.new_chat_photo(self.message)) self.message.new_chat_photo = None self.message.delete_chat_photo = True self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.delete_chat_photo(self.message)) self.message.delete_chat_photo = False self.message.group_chat_created = True self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.chat_created(self.message)) self.message.group_chat_created = False self.message.supergroup_chat_created = True self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.chat_created(self.message)) self.message.supergroup_chat_created = False self.message.migrate_to_chat_id = 100 self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.migrate(self.message)) self.message.migrate_to_chat_id = 0 self.message.migrate_from_chat_id = 100 self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.migrate(self.message)) self.message.migrate_from_chat_id = 0 self.message.channel_chat_created = True self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.chat_created(self.message)) self.message.channel_chat_created = False self.message.pinned_message = 'test' self.assertTrue(Filters.status_update(self.message)) + self.assertTrue(Filters.status_update.pinned_message(self.message)) self.message.pinned_message = None def test_entities_filter(self): @@ -274,6 +285,32 @@ class FiltersTest(BaseTest, unittest.TestCase): with self.assertRaises(NotImplementedError): (custom & Filters.text)(self.message) + def test_language_filter_single(self): + self.message.from_user.language_code = 'en_US' + self.assertTrue((Filters.language('en_US'))(self.message)) + self.assertTrue((Filters.language('en'))(self.message)) + self.assertFalse((Filters.language('en_GB'))(self.message)) + self.assertFalse((Filters.language('da'))(self.message)) + self.message.from_user.language_code = 'en_GB' + self.assertFalse((Filters.language('en_US'))(self.message)) + self.assertTrue((Filters.language('en'))(self.message)) + self.assertTrue((Filters.language('en_GB'))(self.message)) + self.assertFalse((Filters.language('da'))(self.message)) + self.message.from_user.language_code = 'da' + self.assertFalse((Filters.language('en_US'))(self.message)) + self.assertFalse((Filters.language('en'))(self.message)) + self.assertFalse((Filters.language('en_GB'))(self.message)) + self.assertTrue((Filters.language('da'))(self.message)) + + def test_language_filter_multiple(self): + f = Filters.language(['en_US', 'da']) + self.message.from_user.language_code = 'en_US' + self.assertTrue(f(self.message)) + self.message.from_user.language_code = 'en_GB' + self.assertFalse(f(self.message)) + self.message.from_user.language_code = 'da' + self.assertTrue(f(self.message)) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_inlinequeryresultgif.py b/tests/test_inlinequeryresultgif.py index 1d2d6dc48..4c7cf82c1 100644 --- a/tests/test_inlinequeryresultgif.py +++ b/tests/test_inlinequeryresultgif.py @@ -37,6 +37,7 @@ class InlineQueryResultGifTest(BaseTest, unittest.TestCase): self.gif_url = 'gif url' self.gif_width = 10 self.gif_height = 15 + self.gif_duration = 1 self.thumb_url = 'thumb url' self.title = 'title' self.caption = 'caption' @@ -50,6 +51,7 @@ class InlineQueryResultGifTest(BaseTest, unittest.TestCase): 'gif_url': self.gif_url, 'gif_width': self.gif_width, 'gif_height': self.gif_height, + 'gif_duration': self.gif_duration, 'thumb_url': self.thumb_url, 'title': self.title, 'caption': self.caption, @@ -65,6 +67,7 @@ class InlineQueryResultGifTest(BaseTest, unittest.TestCase): self.assertEqual(gif.gif_url, self.gif_url) self.assertEqual(gif.gif_width, self.gif_width) self.assertEqual(gif.gif_height, self.gif_height) + self.assertEqual(gif.gif_duration, self.gif_duration) self.assertEqual(gif.thumb_url, self.thumb_url) self.assertEqual(gif.title, self.title) self.assertEqual(gif.caption, self.caption) diff --git a/tests/test_inlinequeryresultmpeg4gif.py b/tests/test_inlinequeryresultmpeg4gif.py index c0d1d20b5..a9420f99f 100644 --- a/tests/test_inlinequeryresultmpeg4gif.py +++ b/tests/test_inlinequeryresultmpeg4gif.py @@ -37,6 +37,7 @@ class InlineQueryResultMpeg4GifTest(BaseTest, unittest.TestCase): self.mpeg4_url = 'mpeg4 url' self.mpeg4_width = 10 self.mpeg4_height = 15 + self.mpeg4_duration = 1 self.thumb_url = 'thumb url' self.title = 'title' self.caption = 'caption' @@ -50,6 +51,7 @@ class InlineQueryResultMpeg4GifTest(BaseTest, unittest.TestCase): 'mpeg4_url': self.mpeg4_url, 'mpeg4_width': self.mpeg4_width, 'mpeg4_height': self.mpeg4_height, + 'mpeg4_duration': self.mpeg4_duration, 'thumb_url': self.thumb_url, 'title': self.title, 'caption': self.caption, @@ -65,6 +67,7 @@ class InlineQueryResultMpeg4GifTest(BaseTest, unittest.TestCase): self.assertEqual(mpeg4.mpeg4_url, self.mpeg4_url) self.assertEqual(mpeg4.mpeg4_width, self.mpeg4_width) self.assertEqual(mpeg4.mpeg4_height, self.mpeg4_height) + self.assertEqual(mpeg4.mpeg4_duration, self.mpeg4_duration) self.assertEqual(mpeg4.thumb_url, self.thumb_url) self.assertEqual(mpeg4.title, self.title) self.assertEqual(mpeg4.caption, self.caption) diff --git a/tests/test_message.py b/tests/test_message.py index 5109aeab5..240dc7f8f 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -98,16 +98,46 @@ class MessageTest(BaseTest, unittest.TestCase): {entity: 'http://google.com', entity_2: 'h'}) - def test_text_html(self): + def test_text_html_simple(self): test_html_string = 'Test for <bold, ita_lic, code, links and
pre
.' text_html = self.test_message.text_html self.assertEquals(test_html_string, text_html) - def test_text_markdown(self): + def test_text_markdown_simple(self): test_md_string = 'Test for <*bold*, _ita\_lic_, `code`, [links](http://github.com/) and ```pre```.' text_markdown = self.test_message.text_markdown self.assertEquals(test_md_string, text_markdown) + def test_text_html_emoji(self): + text = (b'\\U0001f469\\u200d\\U0001f469\\u200d ABC').decode('unicode-escape') + expected = (b'\\U0001f469\\u200d\\U0001f469\\u200d ABC').decode('unicode-escape') + bold_entity = telegram.MessageEntity(type=telegram.MessageEntity.BOLD, offset=7, length=3) + message = telegram.Message( + message_id=1, from_user=None, date=None, chat=None, text=text, entities=[bold_entity]) + self.assertEquals(expected, message.text_html) + + def test_text_markdown_emoji(self): + text = (b'\\U0001f469\\u200d\\U0001f469\\u200d ABC').decode('unicode-escape') + expected = (b'\\U0001f469\\u200d\\U0001f469\\u200d *ABC*').decode('unicode-escape') + bold_entity = telegram.MessageEntity(type=telegram.MessageEntity.BOLD, offset=7, length=3) + message = telegram.Message( + message_id=1, from_user=None, date=None, chat=None, text=text, entities=[bold_entity]) + self.assertEquals(expected, message.text_markdown) + + def test_parse_entities_url_emoji(self): + url = b'http://github.com/?unicode=\\u2713\\U0001f469'.decode('unicode-escape') + text = 'some url' + link_entity = telegram.MessageEntity(type=telegram.MessageEntity.URL, offset=0, length=8, url=url) + message = telegram.Message( + message_id=1, + from_user=None, + date=None, + chat=None, + text=text, + entities=[link_entity]) + self.assertDictEqual(message.parse_entities(), {link_entity: text}) + self.assertEqual(next(iter(message.parse_entities())).url, url) + @flaky(3, 1) def test_reply_text(self): """Test for Message.reply_text""" diff --git a/tests/test_updater.py b/tests/test_updater.py index 97fcda5d1..d1e3cb84c 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -108,7 +108,7 @@ class UpdaterTest(BaseTest, unittest.TestCase): self.message_count += 1 def telegramHandlerEditedTest(self, bot, update): - self.received_message = update.edited_message.text + self.received_message = update.effective_message.text self.message_count += 1 def telegramInlineHandlerTest(self, bot, update): @@ -188,22 +188,31 @@ class UpdaterTest(BaseTest, unittest.TestCase): self._setup_updater('Test', edited=True) d = self.updater.dispatcher from telegram.ext import Filters - handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, allow_edited=True) + handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, edited_updates=True) d.add_handler(handler) self.updater.start_polling(0.01) sleep(.1) self.assertEqual(self.received_message, 'Test') - # Remove handler - d.remove_handler(handler) - handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, allow_edited=False) - d.add_handler(handler) self.reset() - + d.remove_handler(handler) + handler = MessageHandler( + Filters.text, + self.telegramHandlerEditedTest, + edited_updates=False, + message_updates=False) + d.add_handler(handler) self.updater.bot.send_messages = 1 sleep(.1) self.assertTrue(None is self.received_message) + handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, allow_edited=True) + d.add_handler(handler) + self.reset() + self.updater.bot.send_messages = 1 + sleep(.1) + self.assertEqual(self.received_message, 'Test') + def test_addTelegramMessageHandlerMultipleMessages(self): self._setup_updater('Multiple', 100) self.updater.dispatcher.add_handler(MessageHandler(Filters.all, self.telegramHandlerTest)) diff --git a/tests/test_user.py b/tests/test_user.py index fc89cbf88..960a1cc52 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -37,6 +37,7 @@ class UserTest(BaseTest, unittest.TestCase): self.first_name = "Leandro" self.last_name = "S." self.username = "leandrotoledo" + self.language_code = "pt-BR" self.type = "private" self.json_dict = { @@ -44,6 +45,7 @@ class UserTest(BaseTest, unittest.TestCase): 'first_name': self.first_name, 'last_name': self.last_name, 'username': self.username, + 'language_code': self.language_code, 'type': self.type } @@ -54,6 +56,7 @@ class UserTest(BaseTest, unittest.TestCase): self.assertEqual(user.first_name, self.first_name) self.assertEqual(user.last_name, self.last_name) self.assertEqual(user.username, self.username) + self.assertEqual(user.language_code, self.language_code) self.assertEqual(user.type, self.type) self.assertEqual(user.name, '@leandrotoledo') @@ -98,6 +101,7 @@ class UserTest(BaseTest, unittest.TestCase): self.assertEqual(user['first_name'], self.first_name) self.assertEqual(user['last_name'], self.last_name) self.assertEqual(user['username'], self.username) + self.assertEqual(user['language_code'], self.language_code) self.assertEqual(user['type'], self.type) @flaky(3, 1) diff --git a/tests/test_videonote.py b/tests/test_videonote.py new file mode 100644 index 000000000..5689157bb --- /dev/null +++ b/tests/test_videonote.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2017 +# Leandro Toledo de Souza +# +# 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/]. +"""This module contains an object that represents Tests for Telegram VideoNote""" + +import sys +import unittest +import os + +from flaky import flaky + +sys.path.append('.') + +import telegram +from tests.base import BaseTest, timeout + + +class VideoNoteTest(BaseTest, unittest.TestCase): + """This object represents Tests for Telegram VideoNote.""" + + def setUp(self): + self.videonote_file = open('tests/data/telegram.mp4', 'rb') + self.videonote_file_id = 'DQADAQADBwAD5VIIRYemhHpbPmIQAg' + self.duration = 5 + self.length = 1 # No bloody clue what this does, see note in first test + self.thumb = telegram.PhotoSize.de_json({ + 'file_id': 'AAQBABOMsecvAAQqqoY1Pee_MqcyAAIC', + 'width': 51, + 'file_size': 645, + 'height': 90 + }, self._bot) + self.file_size = 326534 + + self.json_dict = { + 'file_id': self.videonote_file_id, + 'duration': self.duration, + 'length': self.length, + 'thumb': self.thumb.to_dict(), + 'file_size': self.file_size + } + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_required_args_only(self): + # This is where it gets weird.... + # According to telegram length is Video width and height.. but how that works with one + # parameter I couldn't tell you + # It would also seem that it is in fact a required parameter, so the original test below + # fails. Therefore I decided I check for the error instead - that way we'll also know + # when telegram fixes their shit + with self.assertRaisesRegexp(telegram.error.BadRequest, r'Wrong video note length'): + message = self._bot.sendVideoNote(self._chat_id, self.videonote_file, timeout=10) + + # videonote = message.videonote + # + # self.assertTrue(isinstance(videonote.file_id, str)) + # self.assertNotEqual(videonote.file_id, None) + # self.assertEqual(videonote.duration, self.duration) + # self.assertEqual(videonote.length, self.length) + # self.assertEqual(videonote.thumb, self.thumb) + # self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_actual_required_args_only(self): + # See above test... if you pass any number that's > 0 and < some high number, it seems + # to work + message = self._bot.sendVideoNote( + self._chat_id, self.videonote_file, length=self.length, timeout=10) + + videonote = message.video_note + + self.assertTrue(isinstance(videonote.file_id, str)) + self.assertNotEqual(videonote.file_id, None) + self.assertEqual(videonote.duration, self.duration) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_all_args(self): + message = self._bot.sendVideoNote( + self._chat_id, + self.videonote_file, + timeout=10, + duration=self.duration, + length=self.length) + + videonote = message.video_note + + self.assertTrue(isinstance(videonote.file_id, str)) + self.assertNotEqual(videonote.file_id, None) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_resend(self): + message = self._bot.sendVideoNote( + chat_id=self._chat_id, + video_note=self.videonote_file_id, + timeout=10, + duration=self.duration, + length=self.length) + + videonote = message.video_note + + self.assertEqual(videonote.file_id, self.videonote_file_id) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + def test_videonote_de_json(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertEqual(videonote.file_id, self.videonote_file_id) + # self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.file_size, self.file_size) + + def test_videonote_to_json(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_json(videonote.to_json())) + + def test_videonote_to_dict(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_dict(videonote.to_dict())) + self.assertEqual(videonote['file_id'], self.videonote_file_id) + self.assertEqual(videonote['duration'], self.duration) + self.assertEqual(videonote['length'], self.length) + self.assertEqual(videonote['file_size'], self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_empty_file(self): + json_dict = self.json_dict + + del (json_dict['file_id']) + json_dict['video_note'] = open(os.devnull, 'rb') + + self.assertRaises(telegram.TelegramError, + lambda: self._bot.sendVideoNote(chat_id=self._chat_id, + timeout=10, + **json_dict)) + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_empty_file_id(self): + json_dict = self.json_dict + + del (json_dict['file_id']) + json_dict['video_note'] = '' + + self.assertRaises(telegram.TelegramError, + lambda: self._bot.sendVideoNote(chat_id=self._chat_id, + timeout=10, + **json_dict)) + + @flaky(3, 1) + @timeout(10) + def test_reply_videonote(self): + """Test for Message.reply_videonote""" + message = self._bot.sendMessage(self._chat_id, '.') + # Length is needed... see first test + message = message.reply_video_note(self.videonote_file, length=self.length) + + self.assertNotEqual(message.video_note.file_id, None) + + def test_equality(self): + a = telegram.VideoNote(self.videonote_file_id, self.length, self.duration) + b = telegram.VideoNote(self.videonote_file_id, self.length, self.duration) + c = telegram.VideoNote(self.videonote_file_id, 0, 0, 0) + d = telegram.VideoNote("", self.length, self.duration) + e = telegram.Voice(self.videonote_file_id, self.duration) + + self.assertEqual(a, b) + self.assertEqual(hash(a), hash(b)) + self.assertIsNot(a, b) + + self.assertEqual(a, c) + self.assertEqual(hash(a), hash(c)) + + self.assertNotEqual(a, d) + self.assertNotEqual(hash(a), hash(d)) + + self.assertNotEqual(a, e) + self.assertNotEqual(hash(a), hash(e)) + +if __name__ == '__main__': + unittest.main()