From 55e3ecf9f85b019fd4e5f2914ce69835b5ad6199 Mon Sep 17 00:00:00 2001 From: Poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Sun, 29 Mar 2020 00:52:30 -0700 Subject: [PATCH] API 4.6 (#1723) * First take on 4.6 support * improved docs * Minor doc formattings * added poll and poll_answer to filters * added tests, fixed mentioned issues * added poll_answer + poll filter tests * Update docs according to official API docs * introducing pollhandler and pollanswerhandler * First take on 4.6 support * improved docs * Minor doc formattings * added poll and poll_answer to filters * added tests, fixed mentioned issues * added poll_answer + poll filter tests * Update docs according to official API docs * introducing pollhandler and pollanswerhandler * correct_option_id validated with None when trying to send a poll with correct option id 0 it was failing. Now None check is done so that even when 0 is passed it is assigned. * improving example * improving code * adding poll filter example to the pollbot.py * Update Readme * simplify pollbot.py and add some comments * add tests for Poll(Answer)Handler * We just want Filters.poll, not Filters.update.poll * Make test_official fail again * Handle ME.language in M._parse_* Co-authored-by: Hinrich Mahler Co-authored-by: Sharun Kumar <715417+sharunkumar@users.noreply.github.com> --- .github/workflows/test.yml | 1 - README.rst | 2 +- .../telegram.keyboardbuttonpolltype.rst | 6 + docs/source/telegram.pollanswer.rst | 6 + docs/source/telegram.rst | 2 + examples/pollbot.py | 147 ++++++++++++++++ telegram/__init__.py | 7 +- telegram/bot.py | 49 +++++- telegram/ext/__init__.py | 5 +- telegram/ext/pollanswerhandler.py | 85 ++++++++++ telegram/ext/pollhandler.py | 85 ++++++++++ telegram/keyboardbutton.py | 11 +- telegram/keyboardbuttonpolltype.py | 37 ++++ telegram/message.py | 31 +++- telegram/messageentity.py | 14 +- telegram/poll.py | 59 ++++++- telegram/update.py | 21 ++- telegram/user.py | 20 ++- tests/test_bot.py | 15 +- tests/test_keyboardbutton.py | 7 +- tests/test_keyboardbuttonpolltype.py | 47 ++++++ tests/test_message.py | 49 +++--- tests/test_messageentity.py | 7 +- tests/test_poll.py | 59 ++++++- tests/test_pollanswerhandler.py | 158 +++++++++++++++++ tests/test_pollhandler.py | 159 ++++++++++++++++++ tests/test_update.py | 12 +- tests/test_user.py | 21 ++- 28 files changed, 1068 insertions(+), 54 deletions(-) create mode 100644 docs/source/telegram.keyboardbuttonpolltype.rst create mode 100644 docs/source/telegram.pollanswer.rst create mode 100644 examples/pollbot.py create mode 100644 telegram/ext/pollanswerhandler.py create mode 100644 telegram/ext/pollhandler.py create mode 100644 telegram/keyboardbuttonpolltype.py create mode 100644 tests/test_keyboardbuttonpolltype.py create mode 100644 tests/test_pollanswerhandler.py create mode 100644 tests/test_pollhandler.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6adfcb477..d20e92885 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,6 @@ jobs: run: | pytest -v tests/test_official.py exit $? - continue-on-error: True env: TEST_OFFICIAL: "true" shell: bash --noprofile --norc {0} diff --git a/README.rst b/README.rst index 28875f2a7..dc4fcab7d 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **4.5** are supported. +All types and methods of the Telegram Bot API **4.6** are supported. ========== Installing diff --git a/docs/source/telegram.keyboardbuttonpolltype.rst b/docs/source/telegram.keyboardbuttonpolltype.rst new file mode 100644 index 000000000..fa4315abd --- /dev/null +++ b/docs/source/telegram.keyboardbuttonpolltype.rst @@ -0,0 +1,6 @@ +telegram.KeyboardButtonPollType +=============================== + +.. autoclass:: telegram.KeyboardButtonPollType + :members: + :show-inheritance: diff --git a/docs/source/telegram.pollanswer.rst b/docs/source/telegram.pollanswer.rst new file mode 100644 index 000000000..b74899ebf --- /dev/null +++ b/docs/source/telegram.pollanswer.rst @@ -0,0 +1,6 @@ +telegram.PollAnswer +=================== + +.. autoclass:: telegram.PollAnswer + :members: + :show-inheritance: diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index 85d51cabe..718308b9e 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -31,6 +31,7 @@ telegram package telegram.inputmediaphoto telegram.inputmediavideo telegram.keyboardbutton + telegram.keyboardbuttonpolltype telegram.location telegram.loginurl telegram.message @@ -38,6 +39,7 @@ telegram package telegram.parsemode telegram.photosize telegram.poll + telegram.pollanswer telegram.polloption telegram.replykeyboardremove telegram.replykeyboardmarkup diff --git a/examples/pollbot.py b/examples/pollbot.py new file mode 100644 index 000000000..6fd4b10c1 --- /dev/null +++ b/examples/pollbot.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This program is dedicated to the public domain under the CC0 license. + +""" +Basic example for a bot that works with polls. Only 3 people are allowed to interact with each +poll/quiz the bot generates. The preview command generates a closed poll/quiz, excatly like the +one the user sends the bot +""" +import logging + +from telegram import (Poll, ParseMode, KeyboardButton, KeyboardButtonPollType, + ReplyKeyboardMarkup, ReplyKeyboardRemove) +from telegram.ext import (Updater, CommandHandler, PollAnswerHandler, PollHandler, MessageHandler, + Filters) +from telegram.utils.helpers import mention_html + +logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO) +logger = logging.getLogger(__name__) + + +def start(update, context): + """Inform user about what this bot can do""" + update.message.reply_text('Please select /poll to get a Poll, /quiz to get a Quiz or /preview' + ' to generate a preview for your poll') + + +def poll(update, context): + """Sends a predefined poll""" + questions = ["Good", "Really good", "Fantastic", "Great"] + message = context.bot.send_poll(update.effective_user.id, "How are you?", questions, + is_anonymous=False, allows_multiple_answers=True) + # Save some info about the poll the bot_data for later use in receive_poll_answer + payload = {message.poll.id: {"questions": questions, "message_id": message.message_id, + "chat_id": update.effective_chat.id, "answers": 0}} + context.bot_data.update(payload) + + +def receive_poll_answer(update, context): + """Summarize a users poll vote""" + answer = update.poll_answer + poll_id = answer.poll_id + try: + questions = context.bot_data[poll_id]["questions"] + # this means this poll answer update is from an old poll, we can't do our answering then + except KeyError: + return + selected_options = answer.option_ids + answer_string = "" + for question_id in selected_options: + if question_id != selected_options[-1]: + answer_string += questions[question_id] + " and " + else: + answer_string += questions[question_id] + user_mention = mention_html(update.effective_user.id, update.effective_user.full_name) + context.bot.send_message(context.bot_data[poll_id]["chat_id"], + "{} feels {}!".format(user_mention, answer_string), + parse_mode=ParseMode.HTML) + context.bot_data[poll_id]["answers"] += 1 + # Close poll after three participants voted + if context.bot_data[poll_id]["answers"] == 3: + context.bot.stop_poll(context.bot_data[poll_id]["chat_id"], + context.bot_data[poll_id]["message_id"]) + + +def quiz(update, context): + """Send a predefined poll""" + questions = ["1", "2", "4", "20"] + message = update.effective_message.reply_poll("How many eggs do you need for a cake?", + questions, type=Poll.QUIZ, correct_option_id=2) + # Save some info about the poll the bot_data for later use in receive_quiz_answer + payload = {message.poll.id: {"chat_id": update.effective_chat.id, + "message_id": message.message_id}} + context.bot_data.update(payload) + + +def receive_quiz_answer(update, context): + """Close quiz after three participants took it""" + # the bot can receive closed poll updates we don't care about + if update.poll.is_closed: + return + if update.poll.total_voter_count == 3: + try: + quiz_data = context.bot_data[update.poll.id] + # this means this poll answer update is from an old poll, we can't stop it then + except KeyError: + return + context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"]) + + +def preview(update, context): + """Ask user to create a poll and display a preview of it""" + # using this without a type lets the user chooses what he wants (quiz or poll) + button = [[KeyboardButton("Press me!", request_poll=KeyboardButtonPollType())]] + message = "Press the button to let the bot generate a preview for your poll" + # using one_time_keyboard to hide the keyboard + update.effective_message.reply_text(message, + reply_markup=ReplyKeyboardMarkup(button, + one_time_keyboard=True)) + + +def receive_poll(update, context): + """On receiving polls, reply to it by a closed poll copying the received poll""" + actual_poll = update.effective_message.poll + # Only need to set the question and options, since all other parameters don't matter for + # a closed poll + update.effective_message.reply_poll( + question=actual_poll.question, + options=[o.text for o in actual_poll.options], + # with is_closed true, the poll/quiz is immediately closed + is_closed=True, + reply_markup=ReplyKeyboardRemove() + ) + + +def help_handler(update, context): + """Display a help message""" + update.message.reply_text("Use /quiz, /poll or /preview to test this " + "bot.") + + +def main(): + # Create the Updater and pass it your bot's token. + # Make sure to set use_context=True to use the new context based callbacks + # Post version 12 this will no longer be necessary + updater = Updater("TOKEN", use_context=True) + dp = updater.dispatcher + dp.add_handler(CommandHandler('start', start)) + dp.add_handler(CommandHandler('poll', poll)) + dp.add_handler(PollAnswerHandler(receive_poll_answer)) + dp.add_handler(CommandHandler('quiz', quiz)) + dp.add_handler(PollHandler(receive_quiz_answer)) + dp.add_handler(CommandHandler('preview', preview)) + dp.add_handler(MessageHandler(Filters.poll, receive_poll)) + dp.add_handler(CommandHandler('help', help_handler)) + + # Start the Bot + updater.start_polling() + + # Run the bot until the user presses Ctrl-C or the process receives SIGINT, + # SIGTERM or SIGABRT + updater.idle() + + +if __name__ == '__main__': + main() diff --git a/telegram/__init__.py b/telegram/__init__.py index a0e377760..bd22c4804 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -38,6 +38,7 @@ from .files.videonote import VideoNote from .chataction import ChatAction from .userprofilephotos import UserProfilePhotos from .keyboardbutton import KeyboardButton +from .keyboardbuttonpolltype import KeyboardButtonPollType from .replymarkup import ReplyMarkup from .replykeyboardmarkup import ReplyKeyboardMarkup from .replykeyboardremove import ReplyKeyboardRemove @@ -48,7 +49,7 @@ from .files.file import File from .parsemode import ParseMode from .messageentity import MessageEntity from .games.game import Game -from .poll import Poll, PollOption +from .poll import Poll, PollOption, PollAnswer from .loginurl import LoginUrl from .games.callbackgame import CallbackGame from .payment.shippingaddress import ShippingAddress @@ -138,7 +139,7 @@ __all__ = [ 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', 'InlineQueryResultVoice', 'InlineQueryResultGame', 'InputContactMessageContent', 'InputFile', 'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent', - 'InputVenueMessageContent', 'KeyboardButton', 'Location', 'EncryptedCredentials', + 'InputVenueMessageContent', 'Location', 'EncryptedCredentials', 'PassportFile', 'EncryptedPassportElement', 'PassportData', 'Message', 'MessageEntity', 'ParseMode', 'PhotoSize', 'ReplyKeyboardRemove', 'ReplyKeyboardMarkup', 'ReplyMarkup', 'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue', @@ -156,5 +157,5 @@ __all__ = [ 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll', - 'PollOption', 'LoginUrl' + 'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', ] diff --git a/telegram/bot.py b/telegram/bot.py index 131d57669..1589b9419 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -230,6 +230,27 @@ class Bot(TelegramObject): return "https://t.me/{}".format(self.username) + @property + @info + def can_join_groups(self): + """:obj:`str`: Bot's can_join_groups attribute.""" + + return self.bot.can_join_groups + + @property + @info + def can_read_all_group_messages(self): + """:obj:`str`: Bot's can_read_all_group_messages attribute.""" + + return self.bot.can_read_all_group_messages + + @property + @info + def supports_inline_queries(self): + """:obj:`str`: Bot's supports_inline_queries attribute.""" + + return self.bot.supports_inline_queries + @property def name(self): """:obj:`str`: Bot's @username.""" @@ -3492,18 +3513,33 @@ class Bot(TelegramObject): chat_id, question, options, + is_anonymous=True, + type=Poll.REGULAR, + allows_multiple_answers=False, + correct_option_id=None, + is_closed=None, disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None, **kwargs): """ - Use this method to send a native poll. A native poll can't be sent to a private chat. + Use this method to send a native poll. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. question (:obj:`str`): Poll question, 1-255 characters. options (List[:obj:`str`]): List of answer options, 2-10 strings 1-100 characters each. + is_anonymous (:obj:`bool`, optional): True, if the poll needs to be anonymous, + defaults to True. + type (:obj:`str`, optional): Poll type, :attr:`telegram.Poll.QUIZ` or + :attr:`telegram.Poll.REGULAR`, defaults to :attr:`telegram.Poll.REGULAR`. + allows_multiple_answers (:obj:`bool`, optional): True, if the poll allows multiple + answers, ignored for polls in quiz mode, defaults to False + correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer + option, required for polls in quiz mode + is_closed (:obj:`bool`, optional): Pass True, if the poll needs to be immediately + closed. This can be useful for poll preview. 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 @@ -3531,6 +3567,17 @@ class Bot(TelegramObject): 'options': options } + if not is_anonymous: + data['is_anonymous'] = is_anonymous + if type: + data['type'] = type + if allows_multiple_answers: + data['allows_multiple_answers'] = allows_multiple_answers + if correct_option_id is not None: + data['correct_option_id'] = correct_option_id + if is_closed: + data['is_closed'] = is_closed + return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 7528cb3d1..e77b55673 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -41,6 +41,8 @@ from .precheckoutqueryhandler import PreCheckoutQueryHandler from .shippingqueryhandler import ShippingQueryHandler from .messagequeue import MessageQueue from .messagequeue import DelayQueue +from .pollanswerhandler import PollAnswerHandler +from .pollhandler import PollHandler from .defaults import Defaults __all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler', @@ -49,4 +51,5 @@ __all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler', 'StringRegexHandler', 'TypeHandler', 'ConversationHandler', 'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue', 'DispatcherHandlerStop', 'run_async', 'CallbackContext', 'BasePersistence', - 'PicklePersistence', 'DictPersistence', 'PrefixHandler', 'Defaults') + 'PicklePersistence', 'DictPersistence', 'PrefixHandler', 'PollAnswerHandler', + 'PollHandler', 'Defaults') diff --git a/telegram/ext/pollanswerhandler.py b/telegram/ext/pollanswerhandler.py new file mode 100644 index 000000000..7a7ccfed1 --- /dev/null +++ b/telegram/ext/pollanswerhandler.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2019 +# 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/]. + +from telegram import Update +from .handler import Handler + + +class PollAnswerHandler(Handler): + """Handler class to handle Telegram updates that contain a poll answer. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``user_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + + """ + + def check_update(self, update): + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update`): Incoming telegram update. + + Returns: + :obj:`bool` + + """ + return isinstance(update, Update) and update.poll_answer diff --git a/telegram/ext/pollhandler.py b/telegram/ext/pollhandler.py new file mode 100644 index 000000000..e31e942d0 --- /dev/null +++ b/telegram/ext/pollhandler.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2019 +# 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/]. + +from telegram import Update +from .handler import Handler + + +class PollHandler(Handler): + """Handler class to handle Telegram updates that contain a poll. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``user_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + + """ + + def check_update(self, update): + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update`): Incoming telegram update. + + Returns: + :obj:`bool` + + """ + return isinstance(update, Update) and update.poll diff --git a/telegram/keyboardbutton.py b/telegram/keyboardbutton.py index 3d3bbac4d..0b2cf5023 100644 --- a/telegram/keyboardbutton.py +++ b/telegram/keyboardbutton.py @@ -33,6 +33,7 @@ class KeyboardButton(TelegramObject): text (:obj:`str`): Text of the button. request_contact (:obj:`bool`): Optional. If the user's phone number will be sent. request_location (:obj:`bool`): Optional. If the user's current location will be sent. + request_poll (:class:`KeyboardButtonPollType`): Optional. If the user should create a poll. Args: text (:obj:`str`): Text of the button. If none of the optional fields are used, it will be @@ -41,16 +42,24 @@ class KeyboardButton(TelegramObject): a contact when the button is pressed. Available in private chats only. request_location (:obj:`bool`, optional): If True, the user's current location will be sent when the button is pressed. Available in private chats only. + request_poll (:class:`KeyboardButtonPollType`, optional): If specified, the user will be + asked to create a poll and send it to the bot when the button is pressed. Available in + private chats only. Note: :attr:`request_contact` and :attr:`request_location` options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. + :attr:`request_poll` option will only work in Telegram versions released after 23 January, + 2020. Older clients will receive unsupported message. + """ - def __init__(self, text, request_contact=None, request_location=None, **kwargs): + def __init__(self, text, request_contact=None, request_location=None, request_poll=None, + **kwargs): # Required self.text = text # Optionals self.request_contact = request_contact self.request_location = request_location + self.request_poll = request_poll diff --git a/telegram/keyboardbuttonpolltype.py b/telegram/keyboardbuttonpolltype.py new file mode 100644 index 000000000..39c2bb487 --- /dev/null +++ b/telegram/keyboardbuttonpolltype.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2020 +# 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 type of a Telegram Poll.""" +from telegram import TelegramObject + + +class KeyboardButtonPollType(TelegramObject): + """This object represents type of a poll, which is allowed to be created + and sent when the corresponding button is pressed. + + Attributes: + type (:obj:`str`): Optional. If :attr:`telegram.Poll.QUIZ` is passed, the user will be + allowed to create only polls in the quiz mode. If :attr:`telegram.Poll.REGULAR` is + passed, only regular polls will be allowed. Otherwise, the user will be allowed to + create a poll of any type. + """ + def __init__(self, type=None): + self.type = type + + self._id_attrs = (self.type,) diff --git a/telegram/message.py b/telegram/message.py index 1c1f153e6..76c698e90 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -923,6 +923,22 @@ class Message(TelegramObject): return self.bot.delete_message( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) + def stop_poll(self, *args, **kwargs): + """Shortcut for:: + + bot.stop_poll(chat_id=message.chat_id, + message_id=message.message_id, + *args, + **kwargs) + + Returns: + :class:`telegram.Poll`: On success, the stopped Poll with the + final results is returned. + + """ + return self.bot.stop_poll( + chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) + def parse_entity(self, entity): """Returns the text from a given :class:`telegram.MessageEntity`. @@ -1078,7 +1094,11 @@ class Message(TelegramObject): elif entity.type == MessageEntity.CODE: insert = '' + text + '' elif entity.type == MessageEntity.PRE: - insert = '
' + text + '
' + if entity.language: + insert = '
{}
'.format(entity.language, + text) + else: + insert = '
' + text + '
' elif entity.type == MessageEntity.UNDERLINE: insert = '' + text + '' elif entity.type == MessageEntity.STRIKETHROUGH: @@ -1236,10 +1256,13 @@ class Message(TelegramObject): # Monospace needs special escaping. Also can't have entities nested within code = escape_markdown(orig_text, version=version, entity_type=MessageEntity.PRE) - if code.startswith('\\'): - prefix = '```' + if entity.language: + prefix = '```' + entity.language + '\n' else: - prefix = '```\n' + if code.startswith('\\'): + prefix = '```' + else: + prefix = '```\n' insert = prefix + code + '```' elif entity.type == MessageEntity.UNDERLINE: if version == 1: diff --git a/telegram/messageentity.py b/telegram/messageentity.py index 82dfca927..308f5801f 100644 --- a/telegram/messageentity.py +++ b/telegram/messageentity.py @@ -32,6 +32,8 @@ class MessageEntity(TelegramObject): length (:obj:`int`): Length of the entity in UTF-16 code units. url (:obj:`str`): Optional. Url that will be opened after user taps on the text. user (:class:`telegram.User`): Optional. The mentioned user. + language (:obj:`str`): Optional. Programming language of the entity + text Args: type (:obj:`str`): Type of the entity. Can be mention (@username), hashtag, bot_command, @@ -40,13 +42,16 @@ class MessageEntity(TelegramObject): without usernames). offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. length (:obj:`int`): Length of the entity in UTF-16 code units. - url (:obj:`str`, optional): For "text_link" only, url that will be opened after usertaps on - the text. - user (:class:`telegram.User`, optional): For "text_mention" only, the mentioned user. + url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after + usertaps on the text. + user (:class:`telegram.User`, optional): For :attr:`TEXT_MENTION` only, the mentioned + user. + language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of + the entity text """ - def __init__(self, type, offset, length, url=None, user=None, **kwargs): + def __init__(self, type, offset, length, url=None, user=None, language=None, **kwargs): # Required self.type = type self.offset = offset @@ -54,6 +59,7 @@ class MessageEntity(TelegramObject): # Optionals self.url = url self.user = user + self.language = language self._id_attrs = (self.type, self.offset, self.length) diff --git a/telegram/poll.py b/telegram/poll.py index 62cb7a76b..d2b116ee3 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -19,7 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Poll.""" -from telegram import (TelegramObject) +from telegram import (TelegramObject, User) class PollOption(TelegramObject): @@ -48,6 +48,39 @@ class PollOption(TelegramObject): return cls(**data) +class PollAnswer(TelegramObject): + """ + This object represents an answer of a user in a non-anonymous poll. + + Attributes: + poll_id (:obj:`str`): Unique poll identifier. + user (:class:`telegram.User`): The user, who changed the answer to the poll. + option_ids (List[:obj:`int`]): Identifiers of answer options, chosen by the user. + + Args: + poll_id (:obj:`str`): Unique poll identifier. + user (:class:`telegram.User`): The user, who changed the answer to the poll. + option_ids (List[:obj:`int`]): 0-based identifiers of answer options, chosen by the user. + May be empty if the user retracted their vote. + + """ + def __init__(self, poll_id, user, option_ids, **kwargs): + self.poll_id = poll_id + self.user = user + self.option_ids = option_ids + + @classmethod + def de_json(cls, data, bot): + if not data: + return None + + data = super(PollAnswer, cls).de_json(data, bot) + + data['user'] = User.de_json(data.get('user'), bot) + + return cls(**data) + + class Poll(TelegramObject): """ This object contains information about a poll. @@ -56,21 +89,38 @@ class Poll(TelegramObject): id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-255 characters. options (List[:class:`PollOption`]): List of poll options. + total_voter_count (:obj:`int`): Total number of users that voted in the poll. is_closed (:obj:`bool`): True, if the poll is closed. + is_anonymous (:obj:`bool`): True, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): True, if the poll allows multiple answers. + correct_option_id (:obj:`int`): Optional. Identifier of the correct answer option. Args: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-255 characters. options (List[:class:`PollOption`]): List of poll options. is_closed (:obj:`bool`): True, if the poll is closed. + is_anonymous (:obj:`bool`): True, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): True, if the poll allows multiple answers. + correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option. + Available only for polls in the quiz mode, which are closed, or was sent (not + forwarded) by the bot or to the private chat with the bot. """ - def __init__(self, id, question, options, is_closed, **kwargs): + def __init__(self, id, question, options, total_voter_count, is_closed, is_anonymous, type, + allows_multiple_answers, correct_option_id=None, **kwargs): self.id = id self.question = question self.options = options + self.total_voter_count = total_voter_count self.is_closed = is_closed + self.is_anonymous = is_anonymous + self.type = type + self.allows_multiple_answers = allows_multiple_answers + self.correct_option_id = correct_option_id self._id_attrs = (self.id,) @@ -91,3 +141,8 @@ class Poll(TelegramObject): data['options'] = [x.to_dict() for x in self.options] return data + + REGULAR = "regular" + """:obj:`str`: 'regular'""" + QUIZ = "quiz" + """:obj:`str`: 'quiz'""" diff --git a/telegram/update.py b/telegram/update.py index d930a8ab1..499eeba9f 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -20,6 +20,7 @@ from telegram import (Message, TelegramObject, InlineQuery, ChosenInlineResult, CallbackQuery, ShippingQuery, PreCheckoutQuery, Poll) +from telegram.poll import PollAnswer class Update(TelegramObject): @@ -42,7 +43,10 @@ class Update(TelegramObject): pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming pre-checkout query. poll (:class:`telegram.Poll`): Optional. New poll state. Bots receive only updates - about polls, which are sent or stopped by the bot + about stopped polls and polls, which are sent by the bot + poll_answer (:class:`telegram.PollAnswer`): Optional. A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. Args: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a @@ -67,6 +71,9 @@ class Update(TelegramObject): pre-checkout query. Contains full information about checkout poll (:class:`telegram.Poll`, optional): New poll state. Bots receive only updates about polls, which are sent or stopped by the bot + poll_answer (:class:`telegram.PollAnswer`, optional): A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ @@ -83,6 +90,7 @@ class Update(TelegramObject): shipping_query=None, pre_checkout_query=None, poll=None, + poll_answer=None, **kwargs): # Required self.update_id = int(update_id) @@ -97,6 +105,7 @@ class Update(TelegramObject): self.channel_post = channel_post self.edited_channel_post = edited_channel_post self.poll = poll + self.poll_answer = poll_answer self._effective_user = None self._effective_chat = None @@ -137,6 +146,9 @@ class Update(TelegramObject): elif self.pre_checkout_query: user = self.pre_checkout_query.from_user + elif self.poll_answer: + user = self.poll_answer.user + self._effective_user = user return user @@ -146,7 +158,8 @@ class Update(TelegramObject): :class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of update this is. Will be ``None`` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query`, :attr:`pre_checkout_query` and :attr:`poll`. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` and + :attr:`poll_answer`. """ if self._effective_chat: @@ -178,7 +191,8 @@ class Update(TelegramObject): :class:`telegram.Message`: The message included in this update, no matter what kind of update this is. Will be ``None`` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, - :attr:`shipping_query`, :attr:`pre_checkout_query` and :attr:`poll`. + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` and + :attr:`poll_answer`. """ if self._effective_message: @@ -237,5 +251,6 @@ class Update(TelegramObject): edited_channel_post['default_quote'] = data.get('default_quote') data['edited_channel_post'] = Message.de_json(edited_channel_post, bot) data['poll'] = Poll.de_json(data.get('poll'), bot) + data['poll_answer'] = PollAnswer.de_json(data.get('poll_answer'), bot) return cls(**data) diff --git a/telegram/user.py b/telegram/user.py index 2bcfde4a7..084fd65a0 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -34,6 +34,12 @@ class User(TelegramObject): last_name (:obj:`str`): Optional. User's or bot's last name. username (:obj:`str`): Optional. User's or bot's username. language_code (:obj:`str`): Optional. IETF language tag of the user's language. + can_join_groups (:obj:`str`): Optional. True, if the bot can be invited to groups. + Returned only in :attr:`telegram.Bot.get_me` requests. + can_read_all_group_messages (:obj:`str`): Optional. True, if privacy mode is disabled + for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + supports_inline_queries (:obj:`str`): Optional. True, if the bot supports inline queries. + Returned only in :attr:`telegram.Bot.get_me` requests. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. Args: @@ -43,6 +49,12 @@ class User(TelegramObject): last_name (:obj:`str`, optional): User's or bot's last name. username (:obj:`str`, optional): User's or bot's username. language_code (:obj:`str`, optional): IETF language tag of the user's language. + can_join_groups (:obj:`str`, optional): True, if the bot can be invited to groups. + Returned only in :attr:`telegram.Bot.get_me` requests. + can_read_all_group_messages (:obj:`str`, optional): True, if privacy mode is disabled + for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + supports_inline_queries (:obj:`str`, optional): True, if the bot supports inline queries. + Returned only in :attr:`telegram.Bot.get_me` requests. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ @@ -54,6 +66,9 @@ class User(TelegramObject): last_name=None, username=None, language_code=None, + can_join_groups=None, + can_read_all_group_messages=None, + supports_inline_queries=None, bot=None, **kwargs): # Required @@ -64,7 +79,9 @@ class User(TelegramObject): self.last_name = last_name self.username = username self.language_code = language_code - + self.can_join_groups = can_join_groups + self.can_read_all_group_messages = can_read_all_group_messages + self.supports_inline_queries = supports_inline_queries self.bot = bot self._id_attrs = (self.id,) @@ -99,7 +116,6 @@ class User(TelegramObject): def de_json(cls, data, bot): if not data: return None - data = super(User, cls).de_json(data, bot) return cls(bot=bot, **data) diff --git a/tests/test_bot.py b/tests/test_bot.py index ee9bd4fbf..2d755be8e 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -87,6 +87,9 @@ class TestBot(object): assert get_me_bot.first_name == bot.first_name assert get_me_bot.last_name == bot.last_name assert get_me_bot.name == bot.name + assert get_me_bot.can_join_groups == bot.can_join_groups + assert get_me_bot.can_read_all_group_messages == bot.can_read_all_group_messages + assert get_me_bot.supports_inline_queries == bot.supports_inline_queries assert 'https://t.me/{}'.format(get_me_bot.username) == bot.link @flaky(3, 1) @@ -175,14 +178,17 @@ class TestBot(object): question = 'Is this a test?' answers = ['Yes', 'No', 'Maybe'] message = bot.send_poll(chat_id=super_group_id, question=question, options=answers, - timeout=60) + is_anonymous=False, allows_multiple_answers=True, timeout=60) assert message.poll assert message.poll.question == question assert message.poll.options[0].text == answers[0] assert message.poll.options[1].text == answers[1] assert message.poll.options[2].text == answers[2] + assert not message.poll.is_anonymous + assert message.poll.allows_multiple_answers assert not message.poll.is_closed + assert message.poll.type == Poll.REGULAR poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, timeout=60) assert isinstance(poll, Poll) @@ -194,6 +200,13 @@ class TestBot(object): assert poll.options[2].text == answers[2] assert poll.options[2].voter_count == 0 assert poll.question == question + assert poll.total_voter_count == 0 + + message_quiz = bot.send_poll(chat_id=super_group_id, question=question, options=answers, + type=Poll.QUIZ, correct_option_id=2, is_closed=True) + assert message_quiz.poll.correct_option_id == 2 + assert message_quiz.poll.type == Poll.QUIZ + assert message_quiz.poll.is_closed @flaky(3, 1) @pytest.mark.timeout(10) diff --git a/tests/test_keyboardbutton.py b/tests/test_keyboardbutton.py index 1c2b810da..e3da6dac2 100644 --- a/tests/test_keyboardbutton.py +++ b/tests/test_keyboardbutton.py @@ -20,24 +20,28 @@ import pytest from telegram import KeyboardButton +from telegram.keyboardbuttonpolltype import KeyboardButtonPollType @pytest.fixture(scope='class') def keyboard_button(): return KeyboardButton(TestKeyboardButton.text, request_location=TestKeyboardButton.request_location, - request_contact=TestKeyboardButton.request_contact) + request_contact=TestKeyboardButton.request_contact, + request_poll=TestKeyboardButton.request_poll) class TestKeyboardButton(object): text = 'text' request_location = True request_contact = True + request_poll = KeyboardButtonPollType("quiz") def test_expected_values(self, keyboard_button): assert keyboard_button.text == self.text assert keyboard_button.request_location == self.request_location assert keyboard_button.request_contact == self.request_contact + assert keyboard_button.request_poll == self.request_poll def test_to_dict(self, keyboard_button): keyboard_button_dict = keyboard_button.to_dict() @@ -46,3 +50,4 @@ class TestKeyboardButton(object): assert keyboard_button_dict['text'] == keyboard_button.text assert keyboard_button_dict['request_location'] == keyboard_button.request_location assert keyboard_button_dict['request_contact'] == keyboard_button.request_contact + assert keyboard_button_dict['request_poll'] == keyboard_button.request_poll.to_dict() diff --git a/tests/test_keyboardbuttonpolltype.py b/tests/test_keyboardbuttonpolltype.py new file mode 100644 index 000000000..93394ae61 --- /dev/null +++ b/tests/test_keyboardbuttonpolltype.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2020 +# 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/]. +import pytest + +from telegram import KeyboardButtonPollType, Poll + + +@pytest.fixture(scope='class') +def keyboard_button_poll_type(): + return KeyboardButtonPollType(TestKeyboardButtonPollType.type) + + +class TestKeyboardButtonPollType(object): + type = Poll.QUIZ + + def test_to_dict(self, keyboard_button_poll_type): + keyboard_button_poll_type_dict = keyboard_button_poll_type.to_dict() + assert isinstance(keyboard_button_poll_type_dict, dict) + assert keyboard_button_poll_type_dict['type'] == self.type + + def test_equality(self): + a = KeyboardButtonPollType(Poll.QUIZ) + b = KeyboardButtonPollType(Poll.QUIZ) + c = KeyboardButtonPollType(Poll.REGULAR) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/tests/test_message.py b/tests/test_message.py index dd2000c01..3900e9768 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -90,7 +90,9 @@ def message(bot): {'passport_data': PassportData.de_json(RAW_PASSPORT_DATA, None)}, {'poll': Poll(id='abc', question='What is this?', options=[PollOption(text='a', voter_count=1), - PollOption(text='b', voter_count=2)], is_closed=False)}, + PollOption(text='b', voter_count=2)], is_closed=False, + total_voter_count=0, is_anonymous=False, type=Poll.REGULAR, + allows_multiple_answers=True)}, {'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{ 'text': 'start', 'url': 'http://google.com'}, { 'text': 'next', 'callback_data': 'abcd'}], @@ -126,7 +128,7 @@ class TestMessage(object): 'url': 'http://github.com/ab_'}, {'length': 12, 'offset': 38, 'type': 'text_mention', 'user': User(123456789, 'mentioned user', False)}, - {'length': 3, 'offset': 55, 'type': 'pre'}, + {'length': 3, 'offset': 55, 'type': 'pre', 'language': 'python'}, {'length': 21, 'offset': 60, 'type': 'url'}] test_text = 'Test for links, ' 'text-mention and ' '
`\pre
. http://google.com ' - 'and bold nested in strk nested in italic.') + 'and bold nested in strk nested in italic. ' + '
Python pre
.') text_html = self.test_message_v2.text_html assert text_html == test_html_string @@ -227,13 +231,14 @@ class TestMessage(object): ' links, ' 'text-mention and ' '
`\pre
. http://google.com ' - 'and bold nested in strk nested in italic.') + 'and bold nested in strk nested in italic. ' + '
Python pre
.') text_html = self.test_message_v2.text_html_urled assert text_html == test_html_string def test_text_markdown_simple(self): test_md_string = ('Test for <*bold*, _ita_\__lic_, `code`, [links](http://github.com/ab_),' - ' [text-mention](tg://user?id=123456789) and ```\npre```. ' + ' [text-mention](tg://user?id=123456789) and ```python\npre```. ' 'http://google.com/ab\_') text_markdown = self.test_message.text_markdown assert text_markdown == test_md_string @@ -242,7 +247,8 @@ class TestMessage(object): test_md_string = (r'__Test__ for <*bold*, _ita\_lic_, `\\\`code`, ' '[links](http://github.com/abc\\\\\)def), ' '[text\-mention](tg://user?id=123456789) and ```\`\\\\pre```\. ' - 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\.') + 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\. ' + '```python\nPython pre```\.') text_markdown = self.test_message_v2.text_markdown_v2 assert text_markdown == test_md_string @@ -271,7 +277,7 @@ class TestMessage(object): def test_text_markdown_urled(self): test_md_string = ('Test for <*bold*, _ita_\__lic_, `code`, [links](http://github.com/ab_),' - ' [text-mention](tg://user?id=123456789) and ```\npre```. ' + ' [text-mention](tg://user?id=123456789) and ```python\npre```. ' '[http://google.com/ab_](http://google.com/ab_)') text_markdown = self.test_message.text_markdown_urled assert text_markdown == test_md_string @@ -281,7 +287,7 @@ class TestMessage(object): '[links](http://github.com/abc\\\\\)def), ' '[text\-mention](tg://user?id=123456789) and ```\`\\\\pre```\. ' '[http://google\.com](http://google.com) and _bold *nested in ~strk~ ' - 'nested in* italic_\.') + 'nested in* italic_\. ```python\nPython pre```\.') text_markdown = self.test_message_v2.text_markdown_v2_urled assert text_markdown == test_md_string @@ -306,7 +312,8 @@ class TestMessage(object): ' links, ' 'text-mention and ' '
`\pre
. http://google.com ' - 'and bold nested in strk nested in italic.') + 'and bold nested in strk nested in italic. ' + '
Python pre
.') caption_html = self.test_message_v2.caption_html assert caption_html == test_html_string @@ -320,13 +327,14 @@ class TestMessage(object): ' links, ' 'text-mention and ' '
`\pre
. http://google.com ' - 'and bold nested in strk nested in italic.') + 'and bold nested in strk nested in italic. ' + '
Python pre
.') caption_html = self.test_message_v2.caption_html_urled assert caption_html == test_html_string def test_caption_markdown_simple(self): test_md_string = ('Test for <*bold*, _ita_\__lic_, `code`, [links](http://github.com/ab_),' - ' [text-mention](tg://user?id=123456789) and ```\npre```. ' + ' [text-mention](tg://user?id=123456789) and ```python\npre```. ' 'http://google.com/ab\_') caption_markdown = self.test_message.caption_markdown assert caption_markdown == test_md_string @@ -335,7 +343,8 @@ class TestMessage(object): test_md_string = (r'__Test__ for <*bold*, _ita\_lic_, `\\\`code`, ' '[links](http://github.com/abc\\\\\\)def), ' '[text\-mention](tg://user?id=123456789) and ```\`\\\\pre```\. ' - 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\.') + 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\. ' + '```python\nPython pre```\.') caption_markdown = self.test_message_v2.caption_markdown_v2 assert caption_markdown == test_md_string @@ -347,7 +356,7 @@ class TestMessage(object): def test_caption_markdown_urled(self): test_md_string = ('Test for <*bold*, _ita_\__lic_, `code`, [links](http://github.com/ab_),' - ' [text-mention](tg://user?id=123456789) and ```\npre```. ' + ' [text-mention](tg://user?id=123456789) and ```python\npre```. ' '[http://google.com/ab_](http://google.com/ab_)') caption_markdown = self.test_message.caption_markdown_urled assert caption_markdown == test_md_string @@ -357,7 +366,7 @@ class TestMessage(object): '[links](http://github.com/abc\\\\\\)def), ' '[text\-mention](tg://user?id=123456789) and ```\`\\\\pre```\. ' '[http://google\.com](http://google.com) and _bold *nested in ~strk~ ' - 'nested in* italic_\.') + 'nested in* italic_\. ```python\nPython pre```\.') caption_markdown = self.test_message_v2.caption_markdown_v2_urled assert caption_markdown == test_md_string @@ -444,7 +453,7 @@ class TestMessage(object): def test_reply_markdown(self, monkeypatch, message): test_md_string = ('Test for <*bold*, _ita_\__lic_, `code`, [links](http://github.com/ab_),' - ' [text-mention](tg://user?id=123456789) and ```\npre```. ' + ' [text-mention](tg://user?id=123456789) and ```python\npre```. ' 'http://google.com/ab\_') def test(*args, **kwargs): @@ -471,7 +480,8 @@ class TestMessage(object): test_md_string = (r'__Test__ for <*bold*, _ita\_lic_, `\\\`code`, ' '[links](http://github.com/abc\\\\\)def), ' '[text\-mention](tg://user?id=123456789) and ```\`\\\\pre```\. ' - 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\.') + 'http://google\.com and _bold *nested in ~strk~ nested in* italic_\. ' + '```python\nPython pre```\.') def test(*args, **kwargs): cid = args[0] == message.chat_id @@ -498,7 +508,8 @@ class TestMessage(object): ' links, ' 'text-mention and ' '
`\pre
. http://google.com ' - 'and bold nested in strk nested in italic.') + 'and bold nested in strk nested in italic. ' + '
Python pre
.') def test(*args, **kwargs): cid = args[0] == message.chat_id diff --git a/tests/test_messageentity.py b/tests/test_messageentity.py index 333675f40..9ecc68e62 100644 --- a/tests/test_messageentity.py +++ b/tests/test_messageentity.py @@ -32,7 +32,10 @@ def message_entity(request): user = None if type_ == MessageEntity.TEXT_MENTION: user = User(1, 'test_user', False) - return MessageEntity(type, 1, 3, url=url, user=user) + language = None + if type == MessageEntity.PRE: + language = "python" + return MessageEntity(type, 1, 3, url=url, user=user, language=language) class TestMessageEntity(object): @@ -64,6 +67,8 @@ class TestMessageEntity(object): assert entity_dict['url'] == message_entity.url if message_entity.user: assert entity_dict['user'] == message_entity.user.to_dict() + if message_entity.language: + assert entity_dict['language'] == message_entity.language def test_equality(self): a = MessageEntity(MessageEntity.BOLD, 2, 3) diff --git a/tests/test_poll.py b/tests/test_poll.py index 2d824f930..00dc7940c 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -19,7 +19,7 @@ import pytest -from telegram import Poll, PollOption +from telegram import Poll, PollOption, PollAnswer, User @pytest.fixture(scope="class") @@ -50,26 +50,71 @@ class TestPollOption(object): assert poll_option_dict['voter_count'] == poll_option.voter_count +@pytest.fixture(scope="class") +def poll_answer(): + return PollAnswer(poll_id=TestPollAnswer.poll_id, user=TestPollAnswer.user, + option_ids=TestPollAnswer.poll_id) + + +class TestPollAnswer(object): + poll_id = 'id' + user = User(1, '', False) + option_ids = [2] + + def test_de_json(self): + json_dict = { + 'poll_id': self.poll_id, + 'user': self.user.to_dict(), + 'option_ids': self.option_ids + } + poll_answer = PollAnswer.de_json(json_dict, None) + + assert poll_answer.poll_id == self.poll_id + assert poll_answer.user == self.user + assert poll_answer.option_ids == self.option_ids + + def test_to_dict(self, poll_answer): + poll_answer_dict = poll_answer.to_dict() + + assert isinstance(poll_answer_dict, dict) + assert poll_answer_dict['poll_id'] == poll_answer.poll_id + assert poll_answer_dict['user'] == poll_answer.user.to_dict() + assert poll_answer_dict['option_ids'] == poll_answer.option_ids + + @pytest.fixture(scope='class') def poll(): return Poll(TestPoll.id_, TestPoll.question, TestPoll.options, - TestPoll.is_closed) + TestPoll.total_voter_count, + TestPoll.is_closed, + TestPoll.is_anonymous, + TestPoll.type, + TestPoll.allows_multiple_answers + ) class TestPoll(object): id_ = 'id' question = 'Test?' options = [PollOption('test', 10), PollOption('test2', 11)] + total_voter_count = 0 is_closed = True + is_anonymous = False + type = Poll.REGULAR + allows_multiple_answers = True def test_de_json(self): json_dict = { 'id': self.id_, 'question': self.question, 'options': [o.to_dict() for o in self.options], - 'is_closed': self.is_closed + 'total_voter_count': self.total_voter_count, + 'is_closed': self.is_closed, + 'is_anonymous': self.is_anonymous, + 'type': self.type, + 'allows_multiple_answers': self.allows_multiple_answers } poll = Poll.de_json(json_dict, None) @@ -80,7 +125,11 @@ class TestPoll(object): assert poll.options[0].voter_count == self.options[0].voter_count assert poll.options[1].text == self.options[1].text assert poll.options[1].voter_count == self.options[1].voter_count + assert poll.total_voter_count == self.total_voter_count assert poll.is_closed == self.is_closed + assert poll.is_anonymous == self.is_anonymous + assert poll.type == self.type + assert poll.allows_multiple_answers == self.allows_multiple_answers def test_to_dict(self, poll): poll_dict = poll.to_dict() @@ -89,4 +138,8 @@ class TestPoll(object): assert poll_dict['id'] == poll.id assert poll_dict['question'] == poll.question assert poll_dict['options'] == [o.to_dict() for o in poll.options] + assert poll_dict['total_voter_count'] == poll.total_voter_count assert poll_dict['is_closed'] == poll.is_closed + assert poll_dict['is_anonymous'] == poll.is_anonymous + assert poll_dict['type'] == poll.type + assert poll_dict['allows_multiple_answers'] == poll.allows_multiple_answers diff --git a/tests/test_pollanswerhandler.py b/tests/test_pollanswerhandler.py new file mode 100644 index 000000000..d16c403fb --- /dev/null +++ b/tests/test_pollanswerhandler.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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/]. +from queue import Queue + +import pytest + +from telegram import (Update, CallbackQuery, Bot, Message, User, Chat, PollAnswer, + ChosenInlineResult, ShippingQuery, PreCheckoutQuery) +from telegram.ext import PollAnswerHandler, CallbackContext, JobQueue + +message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') + +params = [ + {'message': message}, + {'edited_message': message}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, + {'channel_post': message}, + {'edited_channel_post': message}, + {'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')}, + {'shipping_query': ShippingQuery('id', User(1, '', False), '', None)}, + {'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')} +] + +ids = ('message', 'edited_message', 'callback_query', 'channel_post', + 'edited_channel_post', 'chosen_inline_result', + 'shipping_query', 'pre_checkout_query', 'callback_query_without_message') + + +@pytest.fixture(scope='class', params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope='function') +def poll_answer(bot): + return Update(0, poll_answer=PollAnswer(1, User(2, 'test user', False), [0, 1])) + + +class TestPollAnswerHandler(object): + test_flag = False + + @pytest.fixture(autouse=True) + def reset(self): + self.test_flag = False + + def callback_basic(self, bot, update): + test_bot = isinstance(bot, Bot) + test_update = isinstance(update, Update) + self.test_flag = test_bot and test_update + + def callback_data_1(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) or (chat_data is not None) + + def callback_data_2(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) and (chat_data is not None) + + def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) or (update_queue is not None) + + def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) and (update_queue is not None) + + def callback_context(self, update, context): + self.test_flag = (isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and context.chat_data is None + and isinstance(context.bot_data, dict) + and isinstance(update.poll_answer, PollAnswer)) + + def test_basic(self, dp, poll_answer): + handler = PollAnswerHandler(self.callback_basic) + dp.add_handler(handler) + + assert handler.check_update(poll_answer) + + dp.process_update(poll_answer) + assert self.test_flag + + def test_pass_user_or_chat_data(self, dp, poll_answer): + handler = PollAnswerHandler(self.callback_data_1, pass_user_data=True) + dp.add_handler(handler) + + dp.process_update(poll_answer) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollAnswerHandler(self.callback_data_1, pass_chat_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll_answer) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollAnswerHandler(self.callback_data_2, pass_chat_data=True, + pass_user_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll_answer) + assert self.test_flag + + def test_pass_job_or_update_queue(self, dp, poll_answer): + handler = PollAnswerHandler(self.callback_queue_1, pass_job_queue=True) + dp.add_handler(handler) + + dp.process_update(poll_answer) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollAnswerHandler(self.callback_queue_1, + pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll_answer) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollAnswerHandler(self.callback_queue_2, pass_job_queue=True, + pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll_answer) + assert self.test_flag + + def test_other_update_types(self, false_update): + handler = PollAnswerHandler(self.callback_basic) + assert not handler.check_update(false_update) + + def test_context(self, cdp, poll_answer): + handler = PollAnswerHandler(self.callback_context) + cdp.add_handler(handler) + + cdp.process_update(poll_answer) + assert self.test_flag diff --git a/tests/test_pollhandler.py b/tests/test_pollhandler.py new file mode 100644 index 000000000..2c3012756 --- /dev/null +++ b/tests/test_pollhandler.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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/]. +from queue import Queue + +import pytest + +from telegram import (Update, Poll, PollOption, Bot, Message, User, Chat, CallbackQuery, + ChosenInlineResult, ShippingQuery, PreCheckoutQuery) +from telegram.ext import PollHandler, CallbackContext, JobQueue + +message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') + +params = [ + {'message': message}, + {'edited_message': message}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, + {'channel_post': message}, + {'edited_channel_post': message}, + {'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')}, + {'shipping_query': ShippingQuery('id', User(1, '', False), '', None)}, + {'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')} +] + +ids = ('message', 'edited_message', 'callback_query', 'channel_post', + 'edited_channel_post', 'chosen_inline_result', + 'shipping_query', 'pre_checkout_query', 'callback_query_without_message') + + +@pytest.fixture(scope='class', params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope='function') +def poll(bot): + return Update(0, poll=Poll(1, 'question', [PollOption('1', 0), PollOption('2', 0)], 0, False, + False, Poll.REGULAR, True)) + + +class TestPollHandler(object): + test_flag = False + + @pytest.fixture(autouse=True) + def reset(self): + self.test_flag = False + + def callback_basic(self, bot, update): + test_bot = isinstance(bot, Bot) + test_update = isinstance(update, Update) + self.test_flag = test_bot and test_update + + def callback_data_1(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) or (chat_data is not None) + + def callback_data_2(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) and (chat_data is not None) + + def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) or (update_queue is not None) + + def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) and (update_queue is not None) + + def callback_context(self, update, context): + self.test_flag = (isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, Queue) + and isinstance(context.job_queue, JobQueue) + and context.user_data is None + and context.chat_data is None + and isinstance(context.bot_data, dict) + and isinstance(update.poll, Poll)) + + def test_basic(self, dp, poll): + handler = PollHandler(self.callback_basic) + dp.add_handler(handler) + + assert handler.check_update(poll) + + dp.process_update(poll) + assert self.test_flag + + def test_pass_user_or_chat_data(self, dp, poll): + handler = PollHandler(self.callback_data_1, pass_user_data=True) + dp.add_handler(handler) + + dp.process_update(poll) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollHandler(self.callback_data_1, pass_chat_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollHandler(self.callback_data_2, pass_chat_data=True, + pass_user_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll) + assert self.test_flag + + def test_pass_job_or_update_queue(self, dp, poll): + handler = PollHandler(self.callback_queue_1, pass_job_queue=True) + dp.add_handler(handler) + + dp.process_update(poll) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollHandler(self.callback_queue_1, + pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll) + assert self.test_flag + + dp.remove_handler(handler) + handler = PollHandler(self.callback_queue_2, pass_job_queue=True, + pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(poll) + assert self.test_flag + + def test_other_update_types(self, false_update): + handler = PollHandler(self.callback_basic) + assert not handler.check_update(false_update) + + def test_context(self, cdp, poll): + handler = PollHandler(self.callback_context) + cdp.add_handler(handler) + + cdp.process_update(poll) + assert self.test_flag diff --git a/tests/test_update.py b/tests/test_update.py index fd2cec51c..33af2bbcc 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -21,6 +21,7 @@ import pytest from telegram import (Message, User, Update, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Poll, PollOption) +from telegram.poll import PollAnswer message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') @@ -35,12 +36,13 @@ params = [ {'shipping_query': ShippingQuery('id', User(1, '', False), '', None)}, {'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')}, {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}, - {'poll': Poll('id', '?', [PollOption('.', 1)], False)} + {'poll': Poll('id', '?', [PollOption('.', 1)], False, False, False, Poll.REGULAR, True)}, + {'poll_answer': PollAnswer("id", User(1, '', False), [1])} ] all_types = ('message', 'edited_message', 'callback_query', 'channel_post', 'edited_channel_post', 'inline_query', 'chosen_inline_result', - 'shipping_query', 'pre_checkout_query', 'poll') + 'shipping_query', 'pre_checkout_query', 'poll', 'poll_answer') ids = all_types + ('callback_query_without_message',) @@ -101,7 +103,8 @@ class TestUpdate(object): and update.callback_query.message is None) or update.shipping_query is not None or update.pre_checkout_query is not None - or update.poll is not None): + or update.poll is not None + or update.poll_answer is not None): assert chat.id == 1 else: assert chat is None @@ -125,7 +128,8 @@ class TestUpdate(object): and update.callback_query.message is None) or update.shipping_query is not None or update.pre_checkout_query is not None - or update.poll is not None): + or update.poll is not None + or update.poll_answer is not None): assert eff_message.message_id == message.message_id else: assert eff_message is None diff --git a/tests/test_user.py b/tests/test_user.py index bf1e5e935..bab96aa66 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -30,7 +30,10 @@ def json_dict(): 'first_name': TestUser.first_name, 'last_name': TestUser.last_name, 'username': TestUser.username, - 'language_code': TestUser.language_code + 'language_code': TestUser.language_code, + 'can_join_groups': TestUser.can_join_groups, + 'can_read_all_group_messages': TestUser.can_read_all_group_messages, + 'supports_inline_queries': TestUser.supports_inline_queries } @@ -38,7 +41,9 @@ def json_dict(): def user(bot): return User(id=TestUser.id_, first_name=TestUser.first_name, is_bot=TestUser.is_bot, last_name=TestUser.last_name, username=TestUser.username, - language_code=TestUser.language_code, bot=bot) + language_code=TestUser.language_code, can_join_groups=TestUser.can_join_groups, + can_read_all_group_messages=TestUser.can_read_all_group_messages, + supports_inline_queries=TestUser.supports_inline_queries, bot=bot) class TestUser(object): @@ -48,6 +53,9 @@ class TestUser(object): last_name = u'last\u2022name' username = 'username' language_code = 'en_us' + can_join_groups = True + can_read_all_group_messages = True + supports_inline_queries = False def test_de_json(self, json_dict, bot): user = User.de_json(json_dict, bot) @@ -58,6 +66,9 @@ class TestUser(object): assert user.last_name == self.last_name assert user.username == self.username assert user.language_code == self.language_code + assert user.can_join_groups == self.can_join_groups + assert user.can_read_all_group_messages == self.can_read_all_group_messages + assert user.supports_inline_queries == self.supports_inline_queries def test_de_json_without_username(self, json_dict, bot): del json_dict['username'] @@ -70,6 +81,9 @@ class TestUser(object): assert user.last_name == self.last_name assert user.username is None assert user.language_code == self.language_code + assert user.can_join_groups == self.can_join_groups + assert user.can_read_all_group_messages == self.can_read_all_group_messages + assert user.supports_inline_queries == self.supports_inline_queries def test_de_json_without_username_and_last_name(self, json_dict, bot): del json_dict['username'] @@ -83,6 +97,9 @@ class TestUser(object): assert user.last_name is None assert user.username is None assert user.language_code == self.language_code + assert user.can_join_groups == self.can_join_groups + assert user.can_read_all_group_messages == self.can_read_all_group_messages + assert user.supports_inline_queries == self.supports_inline_queries def test_name(self, user): assert user.name == '@username'