* 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 <hinrich.mahler@freenet.de>
Co-authored-by: Sharun Kumar <715417+sharunkumar@users.noreply.github.com>
This commit is contained in:
Poolitzer 2020-03-29 00:52:30 -07:00 committed by GitHub
parent 8d2c7af1f3
commit 55e3ecf9f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1068 additions and 54 deletions

View file

@ -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}

View file

@ -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

View file

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

View file

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

View file

@ -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

147
examples/pollbot.py Normal file
View file

@ -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()

View file

@ -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',
]

View file

@ -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)

View file

@ -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')

View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
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

View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
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

View file

@ -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

View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a 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,)

View file

@ -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 = '<code>' + text + '</code>'
elif entity.type == MessageEntity.PRE:
insert = '<pre>' + text + '</pre>'
if entity.language:
insert = '<pre><code class="{}">{}</code></pre>'.format(entity.language,
text)
else:
insert = '<pre>' + text + '</pre>'
elif entity.type == MessageEntity.UNDERLINE:
insert = '<u>' + text + '</u>'
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:

View file

@ -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)

View file

@ -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'"""

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
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)

View file

@ -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 <bold, ita_lic, code, links, text-mention and pre. http://google.com/ab_'
test_entities_v2 = [{'length': 4, 'offset': 0, 'type': 'underline'},
@ -141,9 +143,10 @@ class TestMessage(object):
{'length': 17, 'offset': 64, 'type': 'url'},
{'length': 36, 'offset': 86, 'type': 'italic'},
{'length': 24, 'offset': 91, 'type': 'bold'},
{'length': 4, 'offset': 101, 'type': 'strikethrough'}]
{'length': 4, 'offset': 101, 'type': 'strikethrough'},
{'length': 10, 'offset': 124, 'type': 'pre', 'language': 'python'}]
test_text_v2 = ('Test for <bold, ita_lic, \`code, links, text-mention 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 pre.')
test_message = Message(message_id=1,
from_user=None,
date=None,
@ -213,7 +216,8 @@ class TestMessage(object):
' <a href="http://github.com/abc\)def">links</a>, '
'<a href="tg://user?id=123456789">text-mention</a> and '
'<pre>`\pre</pre>. http://google.com '
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>.')
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>. '
'<pre><code class="python">Python pre</code></pre>.')
text_html = self.test_message_v2.text_html
assert text_html == test_html_string
@ -227,13 +231,14 @@ class TestMessage(object):
' <a href="http://github.com/abc\)def">links</a>, '
'<a href="tg://user?id=123456789">text-mention</a> and '
'<pre>`\pre</pre>. <a href="http://google.com">http://google.com</a> '
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>.')
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>. '
'<pre><code class="python">Python pre</code></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):
' <a href="http://github.com/abc\)def">links</a>, '
'<a href="tg://user?id=123456789">text-mention</a> and '
'<pre>`\pre</pre>. http://google.com '
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>.')
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>. '
'<pre><code class="python">Python pre</code></pre>.')
caption_html = self.test_message_v2.caption_html
assert caption_html == test_html_string
@ -320,13 +327,14 @@ class TestMessage(object):
' <a href="http://github.com/abc\)def">links</a>, '
'<a href="tg://user?id=123456789">text-mention</a> and '
'<pre>`\pre</pre>. <a href="http://google.com">http://google.com</a> '
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>.')
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>. '
'<pre><code class="python">Python pre</code></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):
' <a href="http://github.com/abc\)def">links</a>, '
'<a href="tg://user?id=123456789">text-mention</a> and '
'<pre>`\pre</pre>. http://google.com '
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>.')
'and <i>bold <b>nested in <s>strk</s> nested in</b> italic</i>. '
'<pre><code class="python">Python pre</code></pre>.')
def test(*args, **kwargs):
cid = args[0] == message.chat_id

View file

@ -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)

View file

@ -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

View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
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

159
tests/test_pollhandler.py Normal file
View file

@ -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 <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
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

View file

@ -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

View file

@ -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'