Merge branch 'master' into default_parse_mode

This commit is contained in:
Bibo-Joshi 2019-10-14 21:32:58 +02:00 committed by GitHub
commit 0af02eb59d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2150 additions and 777 deletions

View file

@ -66,6 +66,26 @@ Here's how to make a one-off code change.
- You can refer to relevant issues in the commit message by writing, e.g., "#105".
- Your code should adhere to the `PEP 8 Style Guide`_, with the exception that we have a maximum line length of 99.
- Document your code. This project uses `sphinx`_ to generate static HTML docs. To build them, first make sure you have the required dependencies:
.. code-block:: bash
$ pip install -r docs/requirements-docs.txt
then run the following from the PTB root directory:
.. code-block:: bash
$ make -C docs html
or, if you don't have ``make`` available (e.g. on Windows):
.. code-block:: bash
$ sphinx-build docs/source docs/build/html
Once the process terminates, you can view the built documentation by opening ``docs/build/html/index.html`` with a browser.
- For consistency, please conform to `Google Python Style Guide`_ and `Google Python Style Docstrings`_. In addition, code should be formatted consistently with other code around it.
@ -217,6 +237,7 @@ break the API classes. For example:
.. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues
.. _`developers' mailing list`: mailto:devs@python-telegram-bot.org
.. _`PEP 8 Style Guide`: https://www.python.org/dev/peps/pep-0008/
.. _`sphinx`: http://sphinx-doc.org
.. _`Google Python Style Guide`: https://google-styleguide.googlecode.com/svn/trunk/pyguide.html
.. _`Google Python Style Docstrings`: http://sphinx-doc.org/latest/ext/example_google.html
.. _AUTHORS.rst: ../AUTHORS.rst

1
.gitignore vendored
View file

@ -69,6 +69,7 @@ target/
*.sublime*
# unitests files
game.gif
telegram.mp3
telegram.mp4
telegram2.mp4

View file

@ -10,9 +10,6 @@ repos:
sha: 0b70e285e369bcb24b57b74929490ea7be9c4b19
hooks:
- id: flake8
exclude: ^(setup.py|docs/source/conf.py)$
args:
- --ignore=W605,W503
- repo: git://github.com/pre-commit/mirrors-pylint
sha: 9d8dcbc2b86c796275680f239c1e90dcd50bd398
hooks:

View file

@ -2,16 +2,20 @@ language: python
matrix:
include:
- python: 2.7
- python: 3.4
- python: 3.5
- python: 3.6
- python: 3.7
dist: xenial
sudo: true
- python: 3.7
dist: xenial
env: TEST_OFFICIAL=true
- python: pypy2.7-5.10.0
dist: xenial
- python: pypy3.5-5.10.1
dist: xenial
- python: 3.8-dev
dist: xenial
allow_failures:
- python: pypy2.7-5.10.0
- python: pypy3.5-5.10.1
@ -33,16 +37,18 @@ before_cache:
- rm -f $HOME/.pre-commit/pre-commit.log
install:
# fix TypeError from old version of this
- pip install -U codecov pytest-cov
- echo $TRAVIS_PYTHON_VERSION
- if [[ $TRAVIS_PYTHON_VERSION == '3.7'* ]]; then pip install -U git+https://github.com/yaml/pyyaml.git; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.7'* ]]; then pip install -U git+https://github.com/yaml/pyyaml.git; else true; fi
- pip install -U -r requirements.txt
- pip install -U -r requirements-dev.txt
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; fi
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; else true; fi
script:
- pytest -v -m nocoverage
- pytest -v -m "not nocoverage" --cov
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m nocoverage; else true; fi
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m "not nocoverage" --cov; else true; fi
- if [[ $TEST_OFFICIAL == 'true' ]]; then pytest -v tests/test_official.py; else true; fi
after_success:
- coverage combine

View file

@ -4,7 +4,7 @@ Credits
``python-telegram-bot`` was originally created by
`Leandro Toledo <https://github.com/leandrotoledo>`_ and is now maintained by
`Jannes Höke <https://github.com/jh0ker>`_ (`@jh0ker <https://t.me/jh0ker>`_ on Telegram),
`Noam Meltzer <https://github.com/tsnoam>`_, `Pieter Schutz <https://github.com/eldinnie>`_ and `Jasmin Bom <https://github.com/jsmnbom>`_.
`Noam Meltzer <https://github.com/tsnoam>`_, `Pieter Schutz <https://github.com/eldinnie>`_, `Jasmin Bom <https://github.com/jsmnbom>`_ and `Hinrich Mahler <https://github.com/Bibo-Joshi>`_.
We're vendoring urllib3 as part of ``python-telegram-bot`` which is distributed under the MIT
license. For more info, full credits & license terms, the sources can be found here:
@ -65,11 +65,14 @@ The following wonderful people contributed directly or indirectly to this projec
- `Patrick Hofmann <https://github.com/PH89>`_
- `Paul Larsen <https://github.com/PaulSonOfLars>`_
- `Pieter Schutz <https://github.com/eldinnie>`_
- `Poolitzer <https://github.com/Poolitzer>`_
- `Rahiel Kasim <https://github.com/rahiel>`_
- `Sahil Sharma <https://github.com/sahilsharma811>`_
- `Sascha <https://github.com/saschalalala>`_
- `Shelomentsev D <https://github.com/shelomentsevd>`_
- `Simon Schürrle <https://github.com/SitiSchu>`_
- `sooyhwang <https://github.com/sooyhwang>`_
- `syntx <https://github.com/syntx>`_
- `thodnev <https://github.com/thodnev>`_
- `Trainer Jono <https://github.com/Tr-Jono>`_
- `Valentijn <https://github.com/Faalentijn>`_
@ -77,5 +80,6 @@ The following wonderful people contributed directly or indirectly to this projec
- `Vorobjev Simon <https://github.com/simonvorobjev>`_
- `Wagner Macedo <https://github.com/wagnerluis1982>`_
- `wjt <https://github.com/wjt>`_
- `zeshuaro <https://github.com/zeshuaro>`_
Please add yourself here alphabetically when you submit your first pull request.

View file

@ -2,22 +2,86 @@
Changelog
=========
Version 12.0.0b1
Version 12.2.0
==============
**New features:**
- Nested ConversationHandlers (`#1512`_).
**Minor changes, CI improvments or bug fixes:**
- Fix CI failures due to non-backward compat attrs depndency (`#1540`_).
- travis.yaml: TEST_OFFICIAL removed from allowed_failures.
- Fix typos in examples (`#1537`_).
- Fix Bot.to_dict to use proper first_name (`#1525`_).
- Refactor ``test_commandhandler.py`` (`#1408`_).
- Add Python 3.8 (RC version) to Travis testing matrix (`#1543`_).
- test_bot.py: Add to_dict test (`#1544`_).
- Flake config moved into setup.cfg (`#1546`_).
.. _`#1512`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1512
.. _`#1540`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1540
.. _`#1537`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1537
.. _`#1525`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1525
.. _`#1408`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1408
.. _`#1543`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1543
.. _`#1544`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1544
.. _`#1546`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1546
Version 12.1.1
==============
*Released 2019-09-18*
**Hot fix release**
Fixed regression in the vendored urllib3 (`#1517`_).
.. _`#1517`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1517
Version 12.1.0
================
*Released 2019-02-13*
First beta release ever.
It has been so long since last release that we would like to test the impact before a final release.
*We do NOT recommend using this beta release in production.*
*Released 2019-09-13*
**Major changes:**
- Bot API 4.4 support (`#1464`_, `#1510`_)
- Add `get_file` method to `Animation` & `ChatPhoto`. Add, `get_small_file` & `get_big_file`
methods to `ChatPhoto` (`#1489`_)
- Tools for deep linking (`#1049`_)
**Minor changes and/or bug fixes:**
- Documentation fixes (`#1500`_, `#1499`_)
- Improved examples (`#1502`_)
.. _`#1464`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1464
.. _`#1502`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1502
.. _`#1499`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1499
.. _`#1500`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1500
.. _`#1049`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1049
.. _`#1489`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1489
.. _`#1510`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1510
Version 12.0.0
================
*Released 2019-08-29*
Well... This felt like decades. But here we are with a new release.
Expect minor releases soon (mainly complete Bot API 4.4 support)
**Major and/or breaking changes:**
- Context based callbacks
- Persistence
- PrefixHandler added (Handler overhaul)
- Deprecation of RegexHandler and edited_messages, channel_post, etc. arguments (Filter overhaul)
- Various ConversationHandler changes and fixes
- Bot API 4.1, 4.2, 4.3 support
- Python 3.4 is no longer supported
- Error Handler now handles all types of exceptions (`#1485`_)
- Return UTC from from_timestamp() (`#1485`_)
**See the wiki page at https://git.io/fxJuV for a detailed guide on how to migrate from version 11 to version 12.**
@ -64,6 +128,7 @@ ConversationHandler
- Use warnings.warn for ConversationHandler warnings (`#1343`_)
- Fix unresolvable promises (`#1270`_)
Bug fixes & improvements
------------------------
@ -82,6 +147,22 @@ Bug fixes & improvements
- Allow SOCKSConnection to parse username and password from URL (`#1211`_)
- Fix for arguments in passport/data.py (`#1213`_)
- Improve message entity parsing by adding text_mention (`#1206`_)
- Documentation fixes (`#1348`_, `#1397`_, `#1436`_)
- Merged filters short-circuit (`#1350`_)
- Fix webhook listen with tornado (`#1383`_)
- Call task_done() on update queue after update processing finished (`#1428`_)
- Fix send_location() - latitude may be 0 (`#1437`_)
- Make MessageEntity objects comparable (`#1465`_)
- Add prefix to thread names (`#1358`_)
Buf fixes since v12.0.0b1
-------------------------
- Fix setting bot on ShippingQuery (`#1355`_)
- Fix _trigger_timeout() missing 1 required positional argument: 'job' (`#1367`_)
- Add missing message.text check in PrefixHandler check_update (`#1375`_)
- Make updates persist even on DispatcherHandlerStop (`#1463`_)
- Dispatcher force updating persistence object's chat data attribute(`#1462`_)
.. _`#1100`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1100
.. _`#1283`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1283
@ -110,6 +191,22 @@ Bug fixes & improvements
.. _`#1319`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1319
.. _`#1343`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1343
.. _`#1270`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1270
.. _`#1348`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1348
.. _`#1350`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1350
.. _`#1383`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1383
.. _`#1397`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1397
.. _`#1428`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1428
.. _`#1436`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1436
.. _`#1437`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1437
.. _`#1465`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1465
.. _`#1358`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1358
.. _`#1355`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1355
.. _`#1367`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1367
.. _`#1375`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1375
.. _`#1463`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1463
.. _`#1462`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1462
.. _`#1483`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1483
.. _`#1485`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1485
Internal improvements
---------------------
@ -118,6 +215,7 @@ Internal improvements
- Use multiple bots for CI to improve testing times significantly.
- Allow pypy to fail in CI.
- Remove the last CamelCase CheckUpdate methods from the handlers we missed earlier.
- test_official is now executed in a different job
Version 11.1.0
==============

View file

@ -104,21 +104,7 @@ All types and methods of the Telegram Bot API **4.1** are supported.
Installing
==========
**Beta note**
The newest stable release is currently version 11.1.0.
The newest release is a beta release for version 12.
Install or upgrade with:
.. code:: shell
$ pip install python-telegram-bot==12.0.0b1 --upgrade
See CHANGES.rst for the changelog and make sure to report any bugs you find!
You can install or upgrade the stable python-telegram-bot with:
You can install or upgrade python-telegram-bot with:
.. code:: shell

View file

@ -2,7 +2,7 @@ environment:
matrix:
# For Python versions available on Appveyor, see
# http://www.appveyor.com/docs/installed-software#python
# https://www.appveyor.com/docs/windows-images-software/#python
# The list here is complete (excluding Python 2.6, which
# isn't covered by this document) at the time of writing.
@ -10,6 +10,7 @@ environment:
- PYTHON: "C:\\Python35"
- PYTHON: "C:\\Python36"
- PYTHON: "C:\\Python37"
# - PYTHON: "C:\\Python38"
branches:
only:
@ -26,6 +27,8 @@ install:
# Check that we have the expected version and architecture for Python
- "python --version"
# We need wheel installed to build wheels
# fix TypeError from an old version of this
- "pip install attrs==17.4.0"
- "pip install -U codecov pytest-cov"
- "pip install -r requirements.txt"
- "pip install -r requirements-dev.txt"
@ -33,6 +36,7 @@ install:
build: off
test_script:
- "pytest --version"
- "pytest -m \"not nocoverage\" --cov --cov-report xml:coverage.xml"
after_test:

View file

@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '12.0' # telegram.__version__[:3]
version = '12.1' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '12.0.0b1' # telegram.__version__
release = '12.2.0' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -22,7 +22,7 @@ Reference
=========
Below you can find a reference of all the classes and methods in python-telegram-bot.
Apart from the telegram.ext package the objects should reflect the types defined in the `official telegram bot api documentation <https://core.telegram.org/bots/api>`_
Apart from the `telegram.ext` package the objects should reflect the types defined in the `official telegram bot api documentation <https://core.telegram.org/bots/api>`_.
.. toctree::
telegram

View file

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

View file

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

View file

@ -13,6 +13,7 @@ telegram package
telegram.chat
telegram.chataction
telegram.chatmember
telegram.chatpermissions
telegram.chatphoto
telegram.constants
telegram.contact
@ -31,6 +32,7 @@ telegram package
telegram.inputmediavideo
telegram.keyboardbutton
telegram.location
telegram.loginurl
telegram.message
telegram.messageentity
telegram.parsemode
@ -140,8 +142,4 @@ Passport
telegram.encryptedpassportelement
telegram.encryptedcredentials
telegram.utils
--------------
.. toctree::
telegram.utils
.. include:: telegram.utils.rst

View file

@ -16,9 +16,15 @@ A common task for a bot is to ask information from the user. In v5.0 of this lib
### [`conversationbot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.py)
A more complex example of a bot that uses the `ConversationHandler`. It is also more confusing. Good thing there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.png) for this one, too!
### [`nestedconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py)
A even more complex example of a bot that uses the nested `ConversationHandler`s. While it's certainly not that complex that you couldn't built it without nested `ConversationHanldler`s, it gives a good impression on how to work with them. Of course, there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.png) for this example, too!
### [`inlinekeyboard.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard.py)
This example sheds some light on inline keyboards, callback queries and message editing.
### [`inlinekeyboard2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard2.py)
A more complex example about inline keyboards, callback queries and message editing. This example showcases how an interactive menu could be build using inline keyboards.
### [`inlinebot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinebot.py)
A basic example of an [inline bot](https://core.telegram.org/bots/inline). Don't forget to enable inline mode with [@BotFather](https://telegram.me/BotFather).

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to

119
examples/deeplinking.py Normal file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Bot that explains Telegram's "Deep Linking Parameters" functionality.
This program is dedicated to the public domain under the CC0 license.
This Bot uses the Updater class to handle the bot.
First, a few handler functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Deep Linking example. Send /start to get the link.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import Updater, CommandHandler, Filters
# Enable logging
from telegram.utils import helpers
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# Define constants that will allow us to reuse the deep-linking parameters.
CHECK_THIS_OUT = 'check-this-out'
USING_ENTITIES = 'using-entities-here'
SO_COOL = 'so-cool'
def start(update, context):
"""Send a deep-linked URL when the command /start is issued."""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, CHECK_THIS_OUT, group=True)
text = "Feel free to tell your friends about it:\n\n" + url
update.message.reply_text(text)
def deep_linked_level_1(update, context):
"""Reached through the CHECK_THIS_OUT payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, SO_COOL)
text = "Awesome, you just accessed hidden functionality! " \
" Now let's get back to the private chat."
keyboard = InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='Continue here!', url=url)
)
update.message.reply_text(text, reply_markup=keyboard)
def deep_linked_level_2(update, context):
"""Reached through the SO_COOL payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, USING_ENTITIES)
text = "You can also mask the deep-linked URLs as links: " \
"[▶️ CLICK HERE]({0}).".format(url)
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
def deep_linked_level_3(update, context):
"""Reached through the USING_ENTITIES payload"""
payload = context.args
update.message.reply_text("Congratulations! This is as deep as it gets 👏🏻\n\n"
"The payload was: {0}".format(payload))
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
"""Start the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# More info on what deep linking actually is (read this first if it's unclear to you):
# https://core.telegram.org/bots#deep-linking
# Register a deep-linking handler
dp.add_handler(CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT)))
# This one works with a textual link instead of an URL
dp.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL)))
# We can also pass on the deep-linking payload
dp.add_handler(CommandHandler("start",
deep_linked_level_3,
Filters.regex(USING_ENTITIES),
pass_args=True))
# Make sure the deep-linking handlers occur *before* the normal /start handler.
dp.add_handler(CommandHandler("start", start))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()
if __name__ == '__main__':
main()

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Simple Bot to reply to Telegram messages.
@ -30,8 +26,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few handler functions are defined. Then, those functions are passed to
@ -31,8 +27,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Basic example for a bot that uses inline keyboards.

211
examples/inlinekeyboard2.py Normal file
View file

@ -0,0 +1,211 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Simple inline keyboard bot with multiple CallbackQueryHandlers.
This Bot uses the Updater class to handle the bot.
First, a few callback functions are defined as callback query handler. Then, those functions are
passed to the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Example of a bot that uses inline keyboard that has multiple CallbackQueryHandlers arranged in a
ConversationHandler.
Send /start to initiate the conversation.
Press Ctrl-C on the command line to stop the bot.
"""
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, ConversationHandler
import logging
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# Stages
FIRST, SECOND = range(2)
# Callback data
ONE, TWO, THREE, FOUR = range(4)
def start(update, context):
"""Send message on `/start`."""
# Get user that sent /start and log his name
user = update.message.from_user
logger.info("User %s started the conversation.", user.first_name)
# Build InlineKeyboard where each button has a displayed text
# and a string as callback_data
# The keyboard is a list of button rows, where each row is in turn
# a list (hence `[[...]]`).
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Send message with text and appended InlineKeyboard
update.message.reply_text(
"Start handler, Choose a route",
reply_markup=reply_markup
)
# Tell ConversationHandler that we're in state `FIRST` now
return FIRST
def start_over(update, context):
"""Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update
query = update.callback_query
# Get Bot from CallbackContext
bot = context.bot
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Instead of sending a new message, edit the message that
# originated the CallbackQuery. This gives the feeling of an
# interactive menu.
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="Start handler, Choose a route",
reply_markup=reply_markup
)
return FIRST
def one(update, context):
"""Show new choice of buttons"""
query = update.callback_query
bot = context.bot
keyboard = [
[InlineKeyboardButton("3", callback_data=str(THREE)),
InlineKeyboardButton("4", callback_data=str(FOUR))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="First CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
)
return FIRST
def two(update, context):
"""Show new choice of buttons"""
query = update.callback_query
bot = context.bot
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("3", callback_data=str(THREE))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="Second CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
)
return FIRST
def three(update, context):
"""Show new choice of buttons"""
query = update.callback_query
bot = context.bot
keyboard = [
[InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)),
InlineKeyboardButton("Nah, I've had enough ...", callback_data=str(TWO))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="Third CallbackQueryHandler. Do want to start over?",
reply_markup=reply_markup
)
# Transfer to conversation state `SECOND`
return SECOND
def four(update, context):
"""Show new choice of buttons"""
query = update.callback_query
bot = context.bot
keyboard = [
[InlineKeyboardButton("2", callback_data=str(TWO)),
InlineKeyboardButton("4", callback_data=str(FOUR))]
]
reply_markup = InlineKeyboardMarkup(keyboard)
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="Fourth CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
)
return FIRST
def end(update, context):
"""Returns `ConversationHandler.END`, which tells the
ConversationHandler that the conversation is over"""
query = update.callback_query
bot = context.bot
bot.edit_message_text(
chat_id=query.message.chat_id,
message_id=query.message.message_id,
text="See you next time!"
)
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Setup conversation handler with the states FIRST and SECOND
# Use the pattern parameter to pass CallbackQueries with specific
# data pattern to the corresponding handlers.
# ^ means "start of line/string"
# $ means "end of line/string"
# So ^ABC$ will only allow 'ABC'
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
FIRST: [CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$')],
SECOND: [CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(end, pattern='^' + str(TWO) + '$')]
},
fallbacks=[CommandHandler('start', start)]
)
# Add ConversationHandler to dispatcher that will be used for handling
# updates
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

View file

@ -0,0 +1,362 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
"""
First, a few callback functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Example of a bot-user conversation using nested ConversationHandlers.
Send /start to initiate the conversation.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from telegram import (InlineKeyboardMarkup, InlineKeyboardButton)
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
ConversationHandler, CallbackQueryHandler)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# State definitions for top level conversation
SELECTING_ACTION, ADDING_MEMBER, ADDING_SELF, DESCRIBING_SELF = map(chr, range(4))
# State definitions for second level conversation
SELECTING_LEVEL, SELECTING_GENDER = map(chr, range(4, 6))
# State definitions for descriptions conversation
SELECTING_FEATURE, TYPING = map(chr, range(6, 8))
# Meta states
STOPPING, SHOWING = map(chr, range(8, 10))
# Shortcut for ConversationHandler.END
END = ConversationHandler.END
# Different constants for this example
(PARENTS, CHILDREN, SELF, GENDER, MALE, FEMALE, AGE, NAME, START_OVER, FEATURES,
CURRENT_FEATURE, CURRENT_LEVEL) = map(chr, range(10, 22))
# Helper
def _name_switcher(level):
if level == PARENTS:
return ('Father', 'Mother')
elif level == CHILDREN:
return ('Brother', 'Sister')
# Top level conversation callbacks
def start(update, context):
"""Select an action: Adding parent/child or show data."""
text = 'You may add a familiy member, yourself show the gathered data or end the ' \
'conversation. To abort, simply type /stop.'
buttons = [[
InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
InlineKeyboardButton(text='Add yourself', callback_data=str(ADDING_SELF))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Done', callback_data=str(END))
]]
keyboard = InlineKeyboardMarkup(buttons)
# If we're starting over we don't need do send a new message
if context.user_data.get(START_OVER):
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
else:
update.message.reply_text('Hi, I\'m FamiliyBot and here to help you gather information'
'about your family.')
update.message.reply_text(text=text, reply_markup=keyboard)
context.user_data[START_OVER] = False
return SELECTING_ACTION
def adding_self(update, context):
"""Add information about youself."""
context.user_data[CURRENT_LEVEL] = SELF
text = 'Okay, please tell me about yourself.'
button = InlineKeyboardButton(text='Add info', callback_data=str(MALE))
keyboard = InlineKeyboardMarkup.from_button(button)
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
return DESCRIBING_SELF
def show_data(update, context):
"""Pretty print gathered data."""
def prettyprint(user_data, level):
people = user_data.get(level)
if not people:
return '\nNo information yet.'
text = ''
if level == SELF:
for person in user_data[level]:
text += '\nName: {0}, Age: {1}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
else:
male, female = _name_switcher(level)
for person in user_data[level]:
gender = female if person[GENDER] == FEMALE else male
text += '\n{0}: Name: {1}, Age: {2}'.format(gender, person.get(NAME, '-'),
person.get(AGE, '-'))
return text
ud = context.user_data
text = 'Yourself:' + prettyprint(ud, SELF)
text += '\n\nParents:' + prettyprint(ud, PARENTS)
text += '\n\nChildren:' + prettyprint(ud, CHILDREN)
buttons = [[
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
ud[START_OVER] = True
return SHOWING
def stop(update, context):
"""End Conversation by command."""
update.message.reply_text('Okay, bye.')
return END
def end(update, context):
"""End conversation from InlineKeyboardButton."""
text = 'See you around!'
update.callback_query.edit_message_text(text=text)
return END
# Second level conversation callbacks
def select_level(update, context):
"""Choose to add a parent or a child."""
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
buttons = [[
InlineKeyboardButton(text='Add parent', callback_data=str(PARENTS)),
InlineKeyboardButton(text='Add child', callback_data=str(CHILDREN))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
return SELECTING_LEVEL
def select_gender(update, context):
"""Choose to add mother or father."""
level = update.callback_query.data
context.user_data[CURRENT_LEVEL] = level
text = 'Please choose, whom to add.'
male, female = _name_switcher(level)
buttons = [[
InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
return SELECTING_GENDER
def end_second_level(update, context):
"""Return to top level conversation."""
context.user_data[START_OVER] = True
start(update, context)
return END
# Third level callbacks
def select_feature(update, context):
"""Select a feature to update for the person."""
buttons = [[
InlineKeyboardButton(text='Name', callback_data=str(NAME)),
InlineKeyboardButton(text='Age', callback_data=str(AGE)),
InlineKeyboardButton(text='Done', callback_data=str(END)),
]]
keyboard = InlineKeyboardMarkup(buttons)
# If we collect features for a new person, clear the cache and save the gender
if not context.user_data.get(START_OVER):
context.user_data[FEATURES] = {GENDER: update.callback_query.data}
text = 'Please select a feature to update.'
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
# But after we do that, we need to send a new message
else:
text = 'Got it! Please select a feature to update.'
update.message.reply_text(text=text, reply_markup=keyboard)
context.user_data[START_OVER] = False
return SELECTING_FEATURE
def ask_for_input(update, context):
"""Prompt user to input data for selected feature."""
context.user_data[CURRENT_FEATURE] = update.callback_query.data
text = 'Okay, tell me.'
update.callback_query.edit_message_text(text=text)
return TYPING
def save_input(update, context):
"""Save input for feature and return to feature selection."""
ud = context.user_data
ud[FEATURES][ud[CURRENT_FEATURE]] = update.message.text
ud[START_OVER] = True
return select_feature(update, context)
def end_describing(update, context):
"""End gathering of features and return to parent conversation."""
ud = context.user_data
level = ud[CURRENT_LEVEL]
if not ud.get(level):
ud[level] = []
ud[level].append(ud[FEATURES])
# Print upper level menu
if level == SELF:
ud[START_OVER] = True
start(update, context)
else:
select_level(update, context)
return END
def stop_nested(update, context):
"""Completely end conversation from within nested conversation."""
update.message.reply_text('Okay, bye.')
return STOPPING
# Error handler
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
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)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Set up third level ConversationHandler (collecting features)
description_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(select_feature,
pattern='^' + str(MALE) + '$|^' + str(FEMALE) + '$')],
states={
SELECTING_FEATURE: [CallbackQueryHandler(ask_for_input,
pattern='^(?!' + str(END) + ').*$')],
TYPING: [MessageHandler(Filters.text, save_input)],
},
fallbacks=[
CallbackQueryHandler(end_describing, pattern='^' + str(END) + '$'),
CommandHandler('stop', stop_nested)
],
map_to_parent={
# Return to second level menu
END: SELECTING_LEVEL,
# End conversation alltogether
STOPPING: STOPPING,
}
)
# Set up second level ConversationHandler (adding a person)
add_member_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(select_level,
pattern='^' + str(ADDING_MEMBER) + '$')],
states={
SELECTING_LEVEL: [CallbackQueryHandler(select_gender,
pattern='^{0}$|^{1}$'.format(str(PARENTS),
str(CHILDREN)))],
SELECTING_GENDER: [description_conv]
},
fallbacks=[
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
CallbackQueryHandler(end_second_level, pattern='^' + str(END) + '$'),
CommandHandler('stop', stop_nested)
],
map_to_parent={
# After showing data return to top level menu
SHOWING: SHOWING,
# Return to top level menu
END: SELECTING_ACTION,
# End conversation alltogether
STOPPING: END,
}
)
# Set up top level ConversationHandler (selecting action)
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')],
SELECTING_ACTION: [
add_member_conv,
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'),
CallbackQueryHandler(end, pattern='^' + str(END) + '$'),
],
DESCRIBING_SELF: [description_conv],
},
fallbacks=[CommandHandler('stop', stop)],
)
# Because the states of the third level conversation map to the ones of the
# second level conversation, we need to be a bit hacky about that:
conv_handler.states[SELECTING_LEVEL] = conv_handler.states[SELECTING_ACTION]
conv_handler.states[STOPPING] = conv_handler.entry_points
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()
if __name__ == '__main__':
main()

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Simple Bot to print/download all incoming passport data

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Basic example for a bot that can receive payment from user.
@ -107,9 +103,9 @@ def precheckout_callback(update, context):
query.answer(ok=True)
# finally, after contacting to the payment provider...
# finally, after contacting the payment provider...
def successful_payment_callback(update, context):
# do something after successful receive of payment?
# do something after successfully receiving payment?
update.message.reply_text("Thank you for your payment!")

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Simple Bot to send timed Telegram messages.
@ -33,8 +29,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update, context):
update.message.reply_text('Hi! Use /set <seconds> to set a timer')

View file

@ -8,3 +8,4 @@ beautifulsoup4
pytest==4.2.0
pytest-timeout
wheel
attrs==19.1.0

View file

@ -14,7 +14,8 @@ upload-dir = docs/build/html
[flake8]
max-line-length = 99
ignore = W503
ignore = W503, W605
exclude = setup.py, docs/source/conf.py
[yapf]
based_on_style = google

View file

@ -23,6 +23,7 @@ from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
from .chatmember import ChatMember
from .chatpermissions import ChatPermissions
from .files.photosize import PhotoSize
from .files.audio import Audio
from .files.voice import Voice
@ -125,8 +126,8 @@ from .version import __version__ # noqa: F401
__author__ = 'devs@python-telegram-bot.org'
__all__ = [
'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery',
'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatPermissions', 'ChatAction', 'ChosenInlineResult',
'CallbackQuery', 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult',
'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio',
'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif',

View file

@ -1545,7 +1545,8 @@ class Bot(TelegramObject):
calling get_file again.
Args:
file_id (:obj:`str` | :class:`telegram.Audio` | :class:`telegram.Document` | \
file_id (:obj:`str` | :class:`telegram.Animation` | :class:`telegram.Audio` | \
:class:`telegram.ChatPhoto` | :class:`telegram.Document` | \
:class:`telegram.PhotoSize` | :class:`telegram.Sticker` | \
:class:`telegram.Video` | :class:`telegram.VideoNote` | \
:class:`telegram.Voice`):
@ -2720,14 +2721,18 @@ class Bot(TelegramObject):
return result
@log
def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None,
can_send_media_messages=None, can_send_other_messages=None,
can_add_web_page_previews=None, timeout=None, **kwargs):
def restrict_chat_member(self, chat_id, user_id, permissions, until_date=None,
timeout=None, **kwargs):
"""
Use this method to restrict a user in a supergroup. The bot must be an administrator in
the supergroup for this to work and must have the appropriate admin rights. Pass True for
all boolean parameters to lift restrictions from a user.
Note:
Since Bot API 4.4, :attr:`restrict_chat_member` takes the new user permissions in a
single argument of type :class:`telegram.ChatPermissions`. The old way of passing
parameters will not keep working forever.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup (in the format @supergroupusername).
@ -2736,15 +2741,7 @@ class Bot(TelegramObject):
will be lifted for the user, unix time. If user is restricted for more than 366
days or less than 30 seconds from the current time, they are considered to be
restricted forever.
can_send_messages (:obj:`bool`, optional): Pass True, if the user can send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): Pass True, if the user can send
audios, documents, photos, videos, video notes and voice notes, implies
can_send_messages.
can_send_other_messages (:obj:`bool`, optional): Pass True, if the user can send
animations, games, stickers and use inline bots, implies can_send_media_messages.
can_add_web_page_previews (:obj:`bool`, optional): Pass True, if the user may add
web page previews to their messages, implies can_send_media_messages.
permissions (:class:`telegram.ChatPermissions`): New user permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@ -2755,24 +2752,15 @@ class Bot(TelegramObject):
Raises:
:class:`telegram.TelegramError`
"""
url = '{0}/restrictChatMember'.format(self.base_url)
data = {'chat_id': chat_id, 'user_id': user_id}
data = {'chat_id': chat_id, 'user_id': user_id, 'permissions': permissions.to_dict()}
if until_date is not None:
if isinstance(until_date, datetime):
until_date = to_timestamp(until_date)
data['until_date'] = until_date
if can_send_messages is not None:
data['can_send_messages'] = can_send_messages
if can_send_media_messages is not None:
data['can_send_media_messages'] = can_send_media_messages
if can_send_other_messages is not None:
data['can_send_other_messages'] = can_send_other_messages
if can_add_web_page_previews is not None:
data['can_add_web_page_previews'] = can_add_web_page_previews
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
@ -2850,6 +2838,38 @@ class Bot(TelegramObject):
return result
@log
def set_chat_permissions(self, chat_id, permissions, timeout=None, **kwargs):
"""
Use this method to set default chat permissions for all members. The bot must be an
administrator in the group or a supergroup for this to work and must have the
:attr:`can_restrict_members` admin rights. Returns True on success.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
the target supergroup (in the format `@supergroupusername`).
permissions (:class:`telegram.ChatPermissions`): New default chat permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments
Returns:
:obj:`bool`: Returns True on success.
Raises:
:class:`telegram.TelegramError`
"""
url = '{0}/setChatPermissions'.format(self.base_url)
data = {'chat_id': chat_id, 'permissions': permissions.to_dict()}
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
return result
@log
def export_chat_invite_link(self, chat_id, timeout=None, **kwargs):
"""
@ -2993,8 +3013,9 @@ class Bot(TelegramObject):
@log
def set_chat_description(self, chat_id, description, timeout=None, **kwargs):
"""
Use this method to change the description of a supergroup or a channel. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights.
Use this method to change the description of a group, a supergroup or a channel. The bot
must be an administrator in the chat for this to work and must have the appropriate admin
rights.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
@ -3453,7 +3474,7 @@ class Bot(TelegramObject):
return Poll.de_json(result, self)
def to_dict(self):
data = {'id': self.id, 'username': self.username, 'first_name': self.username}
data = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
if self.last_name:
data['last_name'] = self.last_name
@ -3561,6 +3582,8 @@ class Bot(TelegramObject):
"""Alias for :attr:`restrict_chat_member`"""
promoteChatMember = promote_chat_member
"""Alias for :attr:`promote_chat_member`"""
setChatPermissions = set_chat_permissions
"""Alias for :attr:`set_chat_permissions`"""
exportChatInviteLink = export_chat_invite_link
"""Alias for :attr:`export_chat_invite_link`"""
setChatPhoto = set_chat_photo

View file

@ -20,6 +20,7 @@
"""This module contains an object that represents a Telegram Chat."""
from telegram import TelegramObject, ChatPhoto
from .chatpermissions import ChatPermissions
class Chat(TelegramObject):
@ -32,12 +33,13 @@ class Chat(TelegramObject):
username (:obj:`str`): Optional. Username.
first_name (:obj:`str`): Optional. First name of the other party in a private chat.
last_name (:obj:`str`): Optional. Last name of the other party in a private chat.
all_members_are_administrators (:obj:`bool`): Optional.
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
description (:obj:`str`): Optional. Description, for supergroups and channel chats.
description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats.
pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups.
Returned only in get_chat.
permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in getChat.
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the
sticker set.
@ -54,15 +56,15 @@ class Chat(TelegramObject):
available.
first_name(:obj:`str`, optional): First name of the other party in a private chat.
last_name(:obj:`str`, optional): Last name of the other party in a private chat.
all_members_are_administrators (:obj:`bool`, optional): True if a group has `All Members
Are Admins` enabled.
photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in getChat.
description (:obj:`str`, optional): Description, for supergroups and channel chats.
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
Returned only in get_chat.
invite_link (:obj:`str`, optional): Chat invite link, for supergroups and channel chats.
Returned only in get_chat.
pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups.
Returned only in get_chat.
permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in getChat.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set.
Returned only in get_chat.
@ -88,12 +90,12 @@ class Chat(TelegramObject):
username=None,
first_name=None,
last_name=None,
all_members_are_administrators=None,
bot=None,
photo=None,
description=None,
invite_link=None,
pinned_message=None,
permissions=None,
sticker_set_name=None,
can_set_sticker_set=None,
**kwargs):
@ -105,11 +107,13 @@ class Chat(TelegramObject):
self.username = username
self.first_name = first_name
self.last_name = last_name
self.all_members_are_administrators = all_members_are_administrators
# TODO: Remove (also from tests), when Telegram drops this completely
self.all_members_are_administrators = kwargs.get('all_members_are_administrators')
self.photo = photo
self.description = description
self.invite_link = invite_link
self.pinned_message = pinned_message
self.permissions = permissions
self.sticker_set_name = sticker_set_name
self.can_set_sticker_set = can_set_sticker_set
@ -132,6 +136,7 @@ class Chat(TelegramObject):
data['photo'] = ChatPhoto.de_json(data.get('photo'), bot)
from telegram import Message
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot)
return cls(bot=bot, **data)
@ -221,6 +226,17 @@ class Chat(TelegramObject):
"""
return self.bot.unban_chat_member(self.id, *args, **kwargs)
def set_permissions(self, *args, **kwargs):
"""Shortcut for::
bot.set_chat_permissions(update.message.chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent successfully.
"""
return self.bot.set_chat_permissions(self.id, *args, **kwargs)
def send_message(self, *args, **kwargs):
"""Shortcut for::

View file

@ -32,18 +32,17 @@ class ChatMember(TelegramObject):
for this user.
can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator
privileges of that user.
can_change_info (:obj:`bool`): Optional. If the administrator can change the chat title,
photo and other settings.
can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and
other settings.
can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel.
can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other
users.
can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of
other users.
can_invite_users (:obj:`bool`): Optional. If the administrator can invite new users to the
chat.
can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat.
can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or
unban chat members.
can_pin_messages (:obj:`bool`): Optional. If the administrator can pin messages.
can_pin_messages (:obj:`bool`): Optional. If the user can pin messages.
can_promote_members (:obj:`bool`): Optional. If the administrator can add new
administrators.
is_member (:obj:`bool`): Optional. Restricted only. True, if the user is a member of the
@ -52,6 +51,8 @@ class ChatMember(TelegramObject):
locations and venues.
can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages,
implies can_send_messages.
can_send_polls (:obj:`bool`): Optional. True, if the user is allowed to
send polls.
can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games,
stickers and use inline bots, implies can_send_media_messages.
can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his
@ -65,20 +66,20 @@ class ChatMember(TelegramObject):
restrictions will be lifted for this user.
can_be_edited (:obj:`bool`, optional): Administrators only. True, if the bot is allowed to
edit administrator privileges of that user.
can_change_info (:obj:`bool`, optional): Administrators only. True, if the administrator
can change the chat title, photo and other settings.
can_change_info (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can change the chat title, photo and other settings.
can_post_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
can post in the channel, channels only.
can_edit_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
can edit messages of other users, channels only.
can_delete_messages (:obj:`bool`, optional): Administrators only. True, if the
administrator can delete messages of other user.
can_invite_users (:obj:`bool`, optional): Administrators only. True, if the administrator
can invite new users to the chat.
can_invite_users (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can invite new users to the chat.
can_restrict_members (:obj:`bool`, optional): Administrators only. True, if the
administrator can restrict, ban or unban chat members.
can_pin_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
can pin messages, supergroups only.
can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can pin messages, supergroups only.
can_promote_members (:obj:`bool`, optional): Administrators only. True, if the
administrator can add new administrators with a subset of his own privileges or demote
administrators that he has promoted, directly or indirectly (promoted by administrators
@ -90,6 +91,8 @@ class ChatMember(TelegramObject):
can_send_media_messages (:obj:`bool`, optional): Restricted only. True, if the user can
send audios, documents, photos, videos, video notes and voice notes, implies
can_send_messages.
can_send_polls (:obj:`bool`, optional): Restricted only. True, if the user is allowed to
send polls.
can_send_other_messages (:obj:`bool`, optional): Restricted only. True, if the user can
send animations, games, stickers and use inline bots, implies can_send_media_messages.
can_add_web_page_previews (:obj:`bool`, optional): Restricted only. True, if user may add
@ -114,7 +117,7 @@ class ChatMember(TelegramObject):
can_delete_messages=None, can_invite_users=None,
can_restrict_members=None, can_pin_messages=None,
can_promote_members=None, can_send_messages=None,
can_send_media_messages=None, can_send_other_messages=None,
can_send_media_messages=None, can_send_polls=None, can_send_other_messages=None,
can_add_web_page_previews=None, is_member=None, **kwargs):
# Required
self.user = user
@ -131,6 +134,7 @@ class ChatMember(TelegramObject):
self.can_promote_members = can_promote_members
self.can_send_messages = can_send_messages
self.can_send_media_messages = can_send_media_messages
self.can_send_polls = can_send_polls
self.can_send_other_messages = can_send_other_messages
self.can_add_web_page_previews = can_add_web_page_previews
self.is_member = is_member

View file

@ -0,0 +1,87 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatPermission."""
from telegram import TelegramObject
class ChatPermissions(TelegramObject):
"""Describes actions that a non-administrator user is allowed to take in a chat.
Attributes:
can_send_messages (:obj:`bool`): Optional. True, if the user is allowed to send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`): Optional. True, if the user is allowed to send
audios, documents, photos, videos, video notes and voice notes, implies
:attr:`can_send_messages`.
can_send_polls (:obj:`bool`): Optional. True, if the user is allowed to send polls, implies
:attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`): Optional. True, if the user is allowed to send
animations, games, stickers and use inline bots, implies
:attr:`can_send_media_messages`.
can_add_web_page_previews (:obj:`bool`): Optional. True, if the user is allowed to add web
page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`): Optional. True, if the user is allowed to change the chat
title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`): Optional. True, if the user is allowed to invite new users
to the chat.
can_pin_messages (:obj:`bool`): Optional. True, if the user is allowed to pin messages.
Ignored in public supergroups.
Args:
can_send_messages (:obj:`bool`, optional): True, if the user is allowed to send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): True, if the user is allowed to send
audios, documents, photos, videos, video notes and voice notes, implies
:attr:`can_send_messages`.
can_send_polls (:obj:`bool`, optional): True, if the user is allowed to send polls, implies
:attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`, optional): True, if the user is allowed to send
animations, games, stickers and use inline bots, implies
:attr:`can_send_media_messages`.
can_add_web_page_previews (:obj:`bool`, optional): True, if the user is allowed to add web
page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`, optional): True, if the user is allowed to change the chat
title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`, optional): True, if the user is allowed to invite new users
to the chat.
can_pin_messages (:obj:`bool`, optional): True, if the user is allowed to pin messages.
Ignored in public supergroups.
"""
def __init__(self, can_send_messages=None, can_send_media_messages=None, can_send_polls=None,
can_send_other_messages=None, can_add_web_page_previews=None,
can_change_info=None, can_invite_users=None, can_pin_messages=None, **kwargs):
# Required
self.can_send_messages = can_send_messages
self.can_send_media_messages = can_send_media_messages
self.can_send_polls = can_send_polls
self.can_send_other_messages = can_send_other_messages
self.can_add_web_page_previews = can_add_web_page_previews
self.can_change_info = can_change_info
self.can_invite_users = can_invite_users
self.can_pin_messages = can_pin_messages
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)

View file

@ -73,13 +73,31 @@ class CallbackContext(object):
raise ValueError('CallbackContext should not be used with a non context aware '
'dispatcher!')
self._dispatcher = dispatcher
self.chat_data = None
self.user_data = None
self._chat_data = None
self._user_data = None
self.args = None
self.matches = None
self.error = None
self.job = None
@property
def chat_data(self):
return self._chat_data
@chat_data.setter
def chat_data(self, value):
raise AttributeError("You can not assign a new value to chat_data, see "
"https://git.io/fjxKe")
@property
def user_data(self):
return self._user_data
@user_data.setter
def user_data(self, value):
raise AttributeError("You can not assign a new value to user_data, see "
"https://git.io/fjxKe")
@classmethod
def from_error(cls, update, error, dispatcher):
self = cls.from_update(update, dispatcher)
@ -94,9 +112,9 @@ class CallbackContext(object):
user = update.effective_user
if chat:
self.chat_data = dispatcher.chat_data[chat.id]
self._chat_data = dispatcher.chat_data[chat.id]
if user:
self.user_data = dispatcher.user_data[user.id]
self._user_data = dispatcher.user_data[user.id]
return self
@classmethod

View file

@ -57,11 +57,6 @@ class ConversationHandler(Handler):
a regular text message is expected. You could use this for a ``/cancel`` command or to let the
user know their message was not recognized.
The fourth, optional collection of handlers, a ``list`` named :attr:`timed_out_behavior` is
used if the wait for ``run_async`` takes longer than defined in :attr:`run_async_timeout`.
For example, you can let the user know that they should wait for a bit before they can
continue.
To change the state of conversation, the callback function of a handler must return the new
state after responding to the user. If it does not return anything (returning ``None`` by
default), the state will not change. If an entry point callback function returns None,
@ -69,6 +64,20 @@ class ConversationHandler(Handler):
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
Note:
In each of the described collections of handlers, a handler may in turn be a
:class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should
have the attribute :attr:`map_to_parent` which allows to return to the parent conversation
at specified states within the nested conversation.
Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states`
attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents
states to continue the parent conversation after this has ended or even map a state to
:attr:`END` to end the *parent* conversation from within the nested one. For an example on
nested :class:`ConversationHandler` s, see our `examples`_.
.. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
Attributes:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
trigger the start of the conversation.
@ -86,13 +95,16 @@ class ConversationHandler(Handler):
ID.
conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`): Optional. When this handler
is inactive more than this timeout (in seconds), it will be automatically ended. If
this value is 0 (default), there will be no timeout. when it's triggered. The last
this value is 0 (default), there will be no timeout. When it's triggered, the last
received update will be handled by ALL the handler's who's `check_update` method
returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
name (:obj:`str`): Optional. The name for this conversationhandler. Required for
persistence
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
map_to_parent (Dict[:obj:`object`, :obj:`object`]): Optional. A :obj:`dict` that can be
used to instruct a nested conversationhandler to transition into a mapped state on
its parent conversationhandler in place of a specified nested state.
Args:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
@ -124,6 +136,9 @@ class ConversationHandler(Handler):
persistence
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
used to instruct a nested conversationhandler to transition into a mapped state on
its parent conversationhandler in place of a specified nested state.
Raises:
ValueError
@ -147,7 +162,8 @@ class ConversationHandler(Handler):
per_message=False,
conversation_timeout=None,
name=None,
persistent=False):
persistent=False,
map_to_parent=None):
self.entry_points = entry_points
self.states = states
@ -165,6 +181,7 @@ class ConversationHandler(Handler):
self.persistence = None
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
Set by dispatcher"""
self.map_to_parent = map_to_parent
self.timeout_jobs = dict()
self.conversations = dict()
@ -333,7 +350,11 @@ class ConversationHandler(Handler):
self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
self.update_state(new_state, conversation_key)
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
self.update_state(self.END, conversation_key)
return self.map_to_parent.get(new_state)
else:
self.update_state(new_state, conversation_key)
def update_state(self, new_state, key):
if new_state == self.END:

View file

@ -328,15 +328,29 @@ class Dispatcher(object):
if self.persistence.store_chat_data and update.effective_chat:
chat_id = update.effective_chat.id
try:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
except Exception:
self.logger.exception('Saving chat data raised an error')
self.persistence.update_chat_data(chat_id,
self.chat_data[chat_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving chat data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
if self.persistence.store_user_data and update.effective_user:
user_id = update.effective_user.id
try:
self.persistence.update_user_data(user_id, self.user_data[user_id])
except Exception:
self.logger.exception('Saving user data raised an error')
self.persistence.update_user_data(user_id,
self.user_data[user_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving user data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
# An error happened while polling
if isinstance(update, TelegramError):
@ -366,20 +380,17 @@ class Dispatcher(object):
break
# Dispatch any error.
except TelegramError as te:
self.logger.warning('A TelegramError was raised while processing the Update')
except Exception as e:
try:
self.dispatch_error(update, te)
self.dispatch_error(update, e)
except DispatcherHandlerStop:
self.logger.debug('Error handler stopped further handlers')
break
# Errors should not stop the thread.
except Exception:
self.logger.exception('An uncaught error was raised while handling the error')
# Errors should not stop the thread.
except Exception:
self.logger.exception('An uncaught error was raised while processing the update')
self.logger.exception('An error was raised while processing the update and an '
'uncaught error was raised while handling the error '
'with an error_handler')
def add_handler(self, handler, group=DEFAULT_GROUP):
"""Register a handler.
@ -453,11 +464,15 @@ class Dispatcher(object):
self.persistence.update_user_data(user_id, self.user_data[user_id])
def add_error_handler(self, callback):
"""Registers an error handler in the Dispatcher.
"""Registers an error handler in the Dispatcher. This handler will receive every error
which happens in your bot.
Warning: The errors handled within these handlers won't show up in the logger, so you
need to make sure that you reraise the error.
Args:
callback (:obj:`callable`): The callback function for this error handler. Will be
called when an error is raised Callback signature for context based API:
called when an error is raised. Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
@ -483,7 +498,7 @@ class Dispatcher(object):
Args:
update (:obj:`str` | :class:`telegram.Update` | None): The update that caused the error
error (:class:`telegram.TelegramError`): The Telegram error that was raised.
error (:obj:`Exception`): The error that was raised.
"""
if self.error_handlers:

View file

@ -151,14 +151,11 @@ class Handler(object):
optional_args['update_queue'] = dispatcher.update_queue
if self.pass_job_queue:
optional_args['job_queue'] = dispatcher.job_queue
if self.pass_user_data or self.pass_chat_data:
chat = update.effective_chat
if self.pass_user_data:
user = update.effective_user
if self.pass_user_data:
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
if self.pass_chat_data:
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
if self.pass_chat_data:
chat = update.effective_chat
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
return optional_args

View file

@ -56,12 +56,12 @@ class PicklePersistence(BasePersistence):
on any transaction *and* on call fo :meth:`flush`. Default is ``False``.
"""
def __init__(self, filename, store_user_data=True, store_chat_data=True, singe_file=True,
def __init__(self, filename, store_user_data=True, store_chat_data=True, single_file=True,
on_flush=False):
self.filename = filename
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
self.single_file = singe_file
self.single_file = single_file
self.on_flush = on_flush
self.user_data = None
self.chat_data = None

View file

@ -34,6 +34,7 @@ class Animation(TelegramObject):
file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
file_size (:obj:`int`): Optional. File size.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
Args:
file_id (:obj:`str`): Unique file identifier.
@ -44,6 +45,8 @@ class Animation(TelegramObject):
file_name (:obj:`str`, optional): Original animation filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
@ -56,16 +59,19 @@ class Animation(TelegramObject):
file_name=None,
mime_type=None,
file_size=None,
bot=None,
**kwargs):
# Required
self.file_id = str(file_id)
self.width = int(width)
self.height = int(height)
self.duration = duration
# Optionals
self.thumb = thumb
self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
self.bot = bot
self._id_attrs = (self.file_id,)
@ -78,4 +84,22 @@ class Animation(TelegramObject):
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(**data)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:class:`telegram.File`
Raises:
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)

View file

@ -17,9 +17,6 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatPhoto."""
# TODO: add direct download shortcuts.
from telegram import TelegramObject
@ -27,14 +24,14 @@ class ChatPhoto(TelegramObject):
"""This object represents a chat photo.
Attributes:
small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo.
big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo.
small_file_id (:obj:`str`): File identifier of small (160x160) chat photo.
big_file_id (:obj:`str`): File identifier of big (640x640) chat photo.
Args:
small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo. This
file_id can be used only for photo download.
big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo. This file_id
can be used only for photo download.
small_file_id (:obj:`str`): File identifier of small (160x160) chat photo. This file_id can
be used only for photo download and only for as long as the photo is not changed.
big_file_id (:obj:`str`): File identifier of big (640x640) chat photo. This file_id can be
used only for photo download and only for as long as the photo is not changed.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
@ -43,6 +40,9 @@ class ChatPhoto(TelegramObject):
def __init__(self, small_file_id, big_file_id, bot=None, **kwargs):
self.small_file_id = small_file_id
self.big_file_id = big_file_id
self.bot = bot
self._id_attrs = (self.small_file_id, self.big_file_id)
@classmethod
def de_json(cls, data, bot):
@ -50,3 +50,41 @@ class ChatPhoto(TelegramObject):
return None
return cls(bot=bot, **data)
def get_small_file(self, timeout=None, **kwargs):
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
small (160x160) chat photo
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:class:`telegram.File`
Raises:
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.small_file_id, timeout=timeout, **kwargs)
def get_big_file(self, timeout=None, **kwargs):
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
big (640x640) chat photo
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:class:`telegram.File`
Raises:
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.big_file_id, timeout=timeout, **kwargs)

View file

@ -28,6 +28,7 @@ class Sticker(TelegramObject):
file_id (:obj:`str`): Unique identifier for this file.
width (:obj:`int`): Sticker width.
height (:obj:`int`): Sticker height.
is_animated (:obj:`bool`): True, if the sticker is animated.
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg
format.
emoji (:obj:`str`): Optional. Emoji associated with the sticker.
@ -41,6 +42,7 @@ class Sticker(TelegramObject):
file_id (:obj:`str`): Unique identifier for this file.
width (:obj:`int`): Sticker width.
height (:obj:`int`): Sticker height.
is_animated (:obj:`bool`): True, if the sticker is animated.
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .webp or .jpg
format.
emoji (:obj:`str`, optional): Emoji associated with the sticker
@ -58,6 +60,7 @@ class Sticker(TelegramObject):
file_id,
width,
height,
is_animated,
thumb=None,
emoji=None,
file_size=None,
@ -69,6 +72,7 @@ class Sticker(TelegramObject):
self.file_id = str(file_id)
self.width = int(width)
self.height = int(height)
self.is_animated = is_animated
# Optionals
self.thumb = thumb
self.emoji = emoji
@ -123,20 +127,23 @@ class StickerSet(TelegramObject):
Attributes:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
Args:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
"""
def __init__(self, name, title, contains_masks, stickers, bot=None, **kwargs):
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs):
self.name = name
self.title = title
self.is_animated = is_animated
self.contains_masks = contains_masks
self.stickers = stickers

View file

@ -26,7 +26,8 @@ class LoginUrl(TelegramObject):
authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is
coming from Telegram. All the user needs to do is tap/click a button and confirm that they want
to log in. Telegram apps support these buttons as of version 5.7.
Sample bot: @discussbot
Sample bot: `@discussbot <https://t.me/dicussbot>`_
Attributes:
url (:obj:`str`): An HTTP URL to be opened with user authorization data.

View file

@ -865,7 +865,7 @@ class Message(TelegramObject):
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
be an entity that belongs to this message.
Returns:
:obj:`str`: The text of the given entity
@ -890,7 +890,7 @@ class Message(TelegramObject):
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
be an entity that belongs to this message.
Returns:
:obj:`str`: The text of the given entity

View file

@ -87,7 +87,7 @@ def from_timestamp(unixtime):
if not unixtime:
return None
return datetime.fromtimestamp(unixtime)
return datetime.utcfromtimestamp(unixtime)
def mention_html(user_id, name):
@ -147,6 +147,55 @@ def effective_message_type(entity):
return None
def create_deep_linked_url(bot_username, payload=None, group=False):
"""
Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``.
See https://core.telegram.org/bots#deep-linking to learn more.
The ``payload`` may consist of the following characters: ``A-Z, a-z, 0-9, _, -``
Note:
Works well in conjunction with
``CommandHandler("start", callback, filters = Filters.regex('payload'))``
Examples:
``create_deep_linked_url(bot.get_me().username, "some-params")``
Args:
bot_username (:obj:`str`): The username to link to
payload (:obj:`str`, optional): Parameters to encode in the created URL
group (:obj:`bool`, optional): If `True` the user is prompted to select a group to add the
bot to. If `False`, opens a one-on-one conversation with the bot. Defaults to `False`.
Returns:
:obj:`str`: An URL to start the bot with specific parameters
"""
if bot_username is None or len(bot_username) <= 3:
raise ValueError("You must provide a valid bot_username.")
base_url = 'https://t.me/{}'.format(bot_username)
if not payload:
return base_url
if len(payload) > 64:
raise ValueError("The deep-linking payload must not exceed 64 characters.")
if not re.match(r'^[A-Za-z0-9_-]+$', payload):
raise ValueError("Only the following characters are allowed for deep-linked "
"URLs: A-Z, a-z, 0-9, _ and -")
if group:
key = 'startgroup'
else:
key = 'start'
return '{0}?{1}={2}'.format(
base_url,
key,
payload
)
def enocde_conversations_to_json(conversations):
"""Helper method to encode a conversations dict (that uses tuples as keys) to a
JSON-serializable way. Use :attr:`_decode_conversations_from_json` to decode.

View file

@ -17,4 +17,4 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
__version__ = '12.0.0b1'
__version__ = '12.2.0'

View file

@ -16,8 +16,10 @@
#
# 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 datetime
import os
import sys
import re
from collections import defaultdict
from queue import Queue
from threading import Thread, Event
@ -25,8 +27,9 @@ from time import sleep
import pytest
from telegram import Bot
from telegram.ext import Dispatcher, JobQueue, Updater
from telegram import Bot, Message, User, Chat, MessageEntity, Update, \
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter
from tests.bots import get_bot
TRAVIS = os.getenv('TRAVIS', False)
@ -46,7 +49,7 @@ def bot_info():
@pytest.fixture(scope='session')
def bot(bot_info):
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
return make_bot(bot_info)
@pytest.fixture(scope='session')
@ -146,3 +149,107 @@ def pytest_configure(config):
if sys.version_info >= (3,):
config.addinivalue_line('filterwarnings', 'ignore::ResourceWarning')
# TODO: Write so good code that we don't need to ignore ResourceWarnings anymore
def make_bot(bot_info):
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
CMD_PATTERN = re.compile(r'/[\da-z_]{1,32}(?:@\w{1,32})?')
DATE = datetime.datetime.now()
def make_message(text, **kwargs):
"""
Testing utility factory to create a fake ``telegram.Message`` with
reasonable defaults for mimicking a real message.
:param text: (str) message text
:return: a (fake) ``telegram.Message``
"""
return Message(message_id=1,
from_user=kwargs.pop('user', User(id=1, first_name='', is_bot=False)),
date=kwargs.pop('date', DATE),
chat=kwargs.pop('chat', Chat(id=1, type='')),
text=text,
bot=kwargs.pop('bot', make_bot(get_bot())),
**kwargs)
def make_command_message(text, **kwargs):
"""
Testing utility factory to create a message containing a single telegram
command.
Mimics the Telegram API in that it identifies commands within the message
and tags the returned ``Message`` object with the appropriate ``MessageEntity``
tag (but it does this only for commands).
:param text: (str) message text containing (or not) the command
:return: a (fake) ``telegram.Message`` containing only the command
"""
match = re.search(CMD_PATTERN, text)
entities = [MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=match.start(0),
length=len(match.group(0)))] if match else []
return make_message(text, entities=entities, **kwargs)
def make_message_update(message, message_factory=make_message, edited=False, **kwargs):
"""
Testing utility factory to create an update from a message, as either a
``telegram.Message`` or a string. In the latter case ``message_factory``
is used to convert ``message`` to a ``telegram.Message``.
:param message: either a ``telegram.Message`` or a string with the message text
:param message_factory: function to convert the message text into a ``telegram.Message``
:param edited: whether the message should be stored as ``edited_message`` (vs. ``message``)
:return: ``telegram.Update`` with the given message
"""
if not isinstance(message, Message):
message = message_factory(message, **kwargs)
update_kwargs = {'message' if not edited else 'edited_message': message}
return Update(0, **update_kwargs)
def make_command_update(message, edited=False, **kwargs):
"""
Testing utility factory to create an update from a message that potentially
contains a command. See ``make_command_message`` for more details.
:param message: message potentially containing a command
:param edited: whether the message should be stored as ``edited_message`` (vs. ``message``)
:return: ``telegram.Update`` with the given message
"""
return make_message_update(message, make_command_message, edited, **kwargs)
@pytest.fixture(scope='function')
def mock_filter():
class MockFilter(BaseFilter):
def __init__(self):
self.tested = False
def filter(self, message):
self.tested = True
return MockFilter()
def get_false_update_fixture_decorator_params():
message = Message(1, User(1, '', False), DATE, Chat(1, ''), text='test')
params = [
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)},
{'channel_post': message},
{'edited_channel_post': message},
{'inline_query': InlineQuery(1, User(1, '', False), '', '')},
{'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 = tuple(key for kwargs in params for key in kwargs)
return {'params': params, 'ids': ids}
@pytest.fixture(scope='function', **get_false_update_fixture_decorator_params())
def false_update(request):
return Update(update_id=1, **request.param)

View file

@ -17,10 +17,11 @@
# 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 os
import pytest
from flaky import flaky
from telegram import PhotoSize, Animation, Voice
from telegram import PhotoSize, Animation, Voice, TelegramError
from telegram.utils.helpers import escape_markdown
@ -43,6 +44,9 @@ class TestAnimation(object):
width = 320
height = 180
duration = 1
# animation_file_url = 'https://python-telegram-bot.org/static/testfiles/game.gif'
# Shortened link, the above one is cached with the wrong duration.
animation_file_url = 'http://bit.ly/2L18jua'
file_name = 'game.gif.mp4'
mime_type = 'video/mp4'
file_size = 4127
@ -73,17 +77,34 @@ class TestAnimation(object):
assert message.animation.file_name == animation.file_name
assert message.animation.mime_type == animation.mime_type
assert message.animation.file_size == animation.file_size
assert message.animation.thumb.width == 320
assert message.animation.thumb.height == 180
assert message.animation.thumb.width == self.width
assert message.animation.thumb.height == self.height
@flaky(3, 1)
def test_resend(self, bot, chat_id, animation):
message = bot.send_animation(chat_id, animation.file_id)
@pytest.mark.timeout(10)
def test_get_and_download(self, bot, animation):
new_file = bot.get_file(animation.file_id)
assert new_file.file_size == self.file_size
assert new_file.file_id == animation.file_id
assert new_file.file_path.startswith('https://')
new_file.download('game.gif')
assert os.path.isfile('game.gif')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_animation_url_file(self, bot, chat_id, animation):
message = bot.send_animation(chat_id=chat_id, animation=self.animation_file_url,
caption=self.caption)
assert message.caption == self.caption
assert isinstance(message.animation, Animation)
assert isinstance(message.animation.file_id, str)
assert message.animation.file_id != ''
assert message.animation.file_name == animation.file_name
assert message.animation.file_id is not None
assert message.animation.duration == animation.duration
assert message.animation.mime_type == animation.mime_type
assert message.animation.file_size == animation.file_size
@ -129,6 +150,21 @@ class TestAnimation(object):
bot.default_parse_mode = None
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_resend(self, bot, chat_id, animation):
message = bot.send_animation(chat_id, animation.file_id)
assert message.animation == animation
def test_send_with_animation(self, monkeypatch, bot, chat_id, animation):
def test(_, url, data, **kwargs):
return data['animation'] == animation.file_id
monkeypatch.setattr('telegram.utils.request.Request.post', test)
message = bot.send_animation(animation=animation, chat_id=chat_id)
assert message
def test_de_json(self, bot, animation):
json_dict = {
'file_id': self.animation_file_id,
@ -160,6 +196,31 @@ class TestAnimation(object):
assert animation_dict['mime_type'] == animation.mime_type
assert animation_dict['file_size'] == animation.file_size
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_error_send_empty_file(self, bot, chat_id):
animation_file = open(os.devnull, 'rb')
with pytest.raises(TelegramError):
bot.send_animation(chat_id=chat_id, animation=animation_file)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_error_send_empty_file_id(self, bot, chat_id):
with pytest.raises(TelegramError):
bot.send_animation(chat_id=chat_id, animation='')
def test_error_send_without_required_args(self, bot, chat_id):
with pytest.raises(TypeError):
bot.send_animation(chat_id=chat_id)
def test_get_file_instance_method(self, monkeypatch, animation):
def test(*args, **kwargs):
return args[1] == animation.file_id
monkeypatch.setattr('telegram.Bot.get_file', test)
assert animation.get_file()
def test_equality(self):
a = Animation(self.animation_file_id, self.height, self.width, self.duration)
b = Animation(self.animation_file_id, self.height, self.width, self.duration)

View file

@ -28,7 +28,7 @@ from future.utils import string_types
from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup,
InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent,
ShippingOption, LabeledPrice, Poll)
ShippingOption, LabeledPrice, ChatPermissions, Poll)
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
from telegram.utils.helpers import from_timestamp, escape_markdown
@ -50,6 +50,11 @@ def media_message(bot, chat_id):
return bot.send_voice(chat_id, voice=f, caption='my caption', timeout=10)
@pytest.fixture(scope='class')
def chat_permissions():
return ChatPermissions(can_send_messages=False, can_change_info=False, can_invite_users=False)
class TestBot(object):
@pytest.mark.parametrize('token', argvalues=[
'123',
@ -84,6 +89,18 @@ class TestBot(object):
assert get_me_bot.last_name == bot.last_name
assert get_me_bot.name == bot.name
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_to_dict(self, bot):
to_dict_bot = bot.to_dict()
assert isinstance(to_dict_bot, dict)
assert to_dict_bot["id"] == bot.id
assert to_dict_bot["username"] == bot.username
assert to_dict_bot["first_name"] == bot.first_name
if bot.last_name:
assert to_dict_bot["last_name"] == bot.last_name
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_forward_message(self, bot, chat_id, message):
@ -260,6 +277,16 @@ class TestBot(object):
assert bot.unban_chat_member(2, 32)
def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions):
def test(_, url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
permissions = data['permissions'] == chat_permissions.to_dict()
return chat_id and permissions
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.set_chat_permissions(2, chat_permissions)
# TODO: Needs improvement. Need an incoming callbackquery to test
def test_answer_callback_query(self, monkeypatch, bot):
# For now just test that our internals pass the correct data
@ -643,16 +670,13 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_restrict_chat_member(self, bot, channel_id):
def test_restrict_chat_member(self, bot, channel_id, chat_permissions):
# TODO: Add bot to supergroup so this can be tested properly
with pytest.raises(BadRequest, match='Method is available only for supergroups'):
assert bot.restrict_chat_member(channel_id,
95205500,
until_date=datetime.now(),
can_send_messages=False,
can_send_media_messages=False,
can_send_other_messages=False,
can_add_web_page_previews=False)
chat_permissions,
until_date=datetime.now())
@flaky(3, 1)
@pytest.mark.timeout(10)

View file

@ -105,3 +105,15 @@ class TestCallbackContext(object):
callback_context.matches = ['test', 'blah']
assert callback_context.match == 'test'
def test_data_assignment(self, cdp):
update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat')))
callback_context = CallbackContext.from_update(update, cdp)
with pytest.raises(AttributeError):
callback_context.chat_data = {"test": 123}
with pytest.raises(AttributeError):
callback_context.user_data = {}
with pytest.raises(AttributeError):
callback_context.chat_data = "test"

View file

@ -19,7 +19,7 @@
import pytest
from telegram import Chat, ChatAction
from telegram import Chat, ChatAction, ChatPermissions
from telegram import User
@ -28,7 +28,8 @@ def chat(bot):
return Chat(TestChat.id, TestChat.title, TestChat.type, username=TestChat.username,
all_members_are_administrators=TestChat.all_members_are_administrators,
bot=bot, sticker_set_name=TestChat.sticker_set_name,
can_set_sticker_set=TestChat.can_set_sticker_set)
can_set_sticker_set=TestChat.can_set_sticker_set,
permissions=TestChat.permissions)
class TestChat(object):
@ -39,6 +40,11 @@ class TestChat(object):
all_members_are_administrators = False
sticker_set_name = 'stickers'
can_set_sticker_set = False
permissions = ChatPermissions(
can_send_messages=True,
can_change_info=False,
can_invite_users=True,
)
def test_de_json(self, bot):
json_dict = {
@ -48,7 +54,8 @@ class TestChat(object):
'username': self.username,
'all_members_are_administrators': self.all_members_are_administrators,
'sticker_set_name': self.sticker_set_name,
'can_set_sticker_set': self.can_set_sticker_set
'can_set_sticker_set': self.can_set_sticker_set,
'permissions': self.permissions.to_dict()
}
chat = Chat.de_json(json_dict, bot)
@ -59,6 +66,7 @@ class TestChat(object):
assert chat.all_members_are_administrators == self.all_members_are_administrators
assert chat.sticker_set_name == self.sticker_set_name
assert chat.can_set_sticker_set == self.can_set_sticker_set
assert chat.permissions == self.permissions
def test_to_dict(self, chat):
chat_dict = chat.to_dict()
@ -69,6 +77,7 @@ class TestChat(object):
assert chat_dict['type'] == chat.type
assert chat_dict['username'] == chat.username
assert chat_dict['all_members_are_administrators'] == chat.all_members_are_administrators
assert chat_dict['permissions'] == chat.permissions.to_dict()
def test_link(self, chat):
assert chat.link == 'https://t.me/{}'.format(chat.username)
@ -133,6 +142,15 @@ class TestChat(object):
monkeypatch.setattr('telegram.Bot.unban_chat_member', test)
assert chat.unban_member(42)
def test_set_permissions(self, monkeypatch, chat):
def test(*args, **kwargs):
chat_id = args[1] == chat.id
permissions = args[2] == self.permissions
return chat_id and permissions
monkeypatch.setattr('telegram.Bot.set_chat_permissions', test)
assert chat.set_permissions(self.permissions)
def test_instance_method_send_message(self, monkeypatch, chat):
def test(*args, **kwargs):
return args[1] == chat.id and args[2] == 'test'

View file

@ -61,8 +61,9 @@ class TestChatMember(object):
'can_promote_members': True,
'can_send_messages': False,
'can_send_media_messages': True,
'can_send_other_messages': False,
'can_add_web_page_previews': True}
'can_send_polls': False,
'can_send_other_messages': True,
'can_add_web_page_previews': False}
chat_member = ChatMember.de_json(json_dict, bot)
@ -79,8 +80,9 @@ class TestChatMember(object):
assert chat_member.can_promote_members is True
assert chat_member.can_send_messages is False
assert chat_member.can_send_media_messages is True
assert chat_member.can_send_other_messages is False
assert chat_member.can_add_web_page_previews is True
assert chat_member.can_send_polls is False
assert chat_member.can_send_other_messages is True
assert chat_member.can_add_web_page_previews is False
def test_to_dict(self, chat_member):
chat_member_dict = chat_member.to_dict()

View file

@ -0,0 +1,79 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# 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 ChatPermissions
@pytest.fixture(scope="class")
def chat_permissions():
return ChatPermissions(can_send_messages=True, can_send_media_messages=True,
can_send_polls=True, can_send_other_messages=True,
can_add_web_page_previews=True, can_change_info=True,
can_invite_users=True, can_pin_messages=True)
class TestChatPermissions(object):
can_send_messages = True
can_send_media_messages = True
can_send_polls = True
can_send_other_messages = False
can_add_web_page_previews = False
can_change_info = False
can_invite_users = None
can_pin_messages = None
def test_de_json(self, bot):
json_dict = {
'can_send_messages': self.can_send_messages,
'can_send_media_messages': self.can_send_media_messages,
'can_send_polls': self.can_send_polls,
'can_send_other_messages': self.can_send_other_messages,
'can_add_web_page_previews': self.can_add_web_page_previews,
'can_change_info': self.can_change_info,
'can_invite_users': self.can_invite_users,
'can_pin_messages': self.can_pin_messages
}
permissions = ChatPermissions.de_json(json_dict, bot)
assert permissions.can_send_messages == self.can_send_messages
assert permissions.can_send_media_messages == self.can_send_media_messages
assert permissions.can_send_polls == self.can_send_polls
assert permissions.can_send_other_messages == self.can_send_other_messages
assert permissions.can_add_web_page_previews == self.can_add_web_page_previews
assert permissions.can_change_info == self.can_change_info
assert permissions.can_invite_users == self.can_invite_users
assert permissions.can_pin_messages == self.can_pin_messages
def test_to_dict(self, chat_permissions):
permissions_dict = chat_permissions.to_dict()
assert isinstance(permissions_dict, dict)
assert permissions_dict['can_send_messages'] == chat_permissions.can_send_messages
assert (permissions_dict['can_send_media_messages']
== chat_permissions.can_send_media_messages)
assert permissions_dict['can_send_polls'] == chat_permissions.can_send_polls
assert (permissions_dict['can_send_other_messages']
== chat_permissions.can_send_other_messages)
assert (permissions_dict['can_add_web_page_previews']
== chat_permissions.can_add_web_page_previews)
assert permissions_dict['can_change_info'] == chat_permissions.can_change_info
assert permissions_dict['can_invite_users'] == chat_permissions.can_invite_users
assert permissions_dict['can_pin_messages'] == chat_permissions.can_pin_messages

View file

@ -21,50 +21,30 @@ import re
from queue import Queue
import pytest
import itertools
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery,
ChosenInlineResult, ShippingQuery, PreCheckoutQuery, MessageEntity)
from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue, \
PrefixHandler
message = Message(1, User(1, '', False), None, Chat(1, ''), text='test')
params = [
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)},
{'channel_post': message},
{'edited_channel_post': message},
{'inline_query': InlineQuery(1, User(1, '', False), '', '')},
{'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 = ('callback_query', 'channel_post', 'edited_channel_post', 'inline_query',
'chosen_inline_result', 'shipping_query', 'pre_checkout_query',
'callback_query_without_message',)
from telegram import Message, Update, Chat, Bot
from telegram.ext import CommandHandler, Filters, CallbackContext, JobQueue, PrefixHandler
from tests.conftest import make_command_message, make_command_update, make_message, \
make_message_update
@pytest.fixture(scope='class', params=params, ids=ids)
def false_update(request):
return Update(update_id=1, **request.param)
def is_match(handler, update):
"""
Utility function that returns whether an update matched
against a specific handler.
:param handler: ``CommandHandler`` to check against
:param update: update to check
:return: (bool) whether ``update`` matched with ``handler``
"""
check = handler.check_update(update)
return check is not None and check is not False
@pytest.fixture(scope='function')
def message(bot):
return Message(message_id=1,
from_user=User(id=1, first_name='', is_bot=False),
date=None,
chat=Chat(id=1, type=''),
message='/test',
bot=bot,
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/test'))])
class TestCommandHandler(object):
class BaseTest(object):
"""Base class for command and prefix handler test classes. Contains
utility methods an several callbacks used by both classes."""
test_flag = False
SRE_TYPE = type(re.match("", ""))
@ -72,30 +52,33 @@ class TestCommandHandler(object):
def reset(self):
self.test_flag = False
PASS_KEYWORDS = ('pass_user_data', 'pass_chat_data', 'pass_job_queue', 'pass_update_queue')
@pytest.fixture(scope='module', params=itertools.combinations(PASS_KEYWORDS, 2))
def pass_combination(self, request):
return {key: True for key in request.param}
def response(self, dispatcher, update):
"""
Utility to send an update to a dispatcher and assert
whether the callback was called appropriately. Its purpose is
for repeated usage in the same test function.
"""
self.test_flag = False
dispatcher.process_update(update)
return self.test_flag
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 make_callback_for(self, pass_keyword):
def callback(bot, update, **kwargs):
self.test_flag = kwargs.get(keyword, None) 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 ch_callback_args(self, bot, update, args):
if update.message.text == '/test':
self.test_flag = len(args) == 0
elif update.message.text == '/test@{}'.format(bot.username):
self.test_flag = len(args) == 0
else:
self.test_flag = args == ['one', 'two']
keyword = pass_keyword[5:]
return callback
def callback_context(self, update, context):
self.test_flag = (isinstance(context, CallbackContext)
@ -122,550 +105,295 @@ class TestCommandHandler(object):
num = len(context.matches) == 2
self.test_flag = types and num
def test_basic(self, dp, message):
handler = CommandHandler('test', self.callback_basic)
def _test_context_args_or_regex(self, cdp, handler, text):
cdp.add_handler(handler)
update = make_command_update(text)
assert not self.response(cdp, update)
update.message.text += ' one two'
assert self.response(cdp, update)
def _test_edited(self, message, handler_edited, handler_not_edited):
"""
Assert whether a handler that should accept edited messages
and a handler that shouldn't work correctly.
:param message: ``telegram.Message`` to check against the handlers
:param handler_edited: handler that should accept edited messages
:param handler_not_edited: handler that should not accept edited messages
"""
update = make_command_update(message)
edited_update = make_command_update(message, edited=True)
assert is_match(handler_edited, update)
assert is_match(handler_edited, edited_update)
assert is_match(handler_not_edited, update)
assert not is_match(handler_not_edited, edited_update)
# ----------------------------- CommandHandler -----------------------------
class TestCommandHandler(BaseTest):
CMD = '/test'
@pytest.fixture(scope='class')
def command(self):
return self.CMD
@pytest.fixture(scope='class')
def command_message(self, command):
return make_command_message(command)
@pytest.fixture(scope='class')
def command_update(self, command_message):
return make_command_update(command_message)
def ch_callback_args(self, bot, update, args):
if update.message.text == self.CMD:
self.test_flag = len(args) == 0
elif update.message.text == '{}@{}'.format(self.CMD, bot.username):
self.test_flag = len(args) == 0
else:
self.test_flag = args == ['one', 'two']
def make_default_handler(self, callback=None, **kwargs):
callback = callback or self.callback_basic
return CommandHandler(self.CMD[1:], callback, **kwargs)
def test_basic(self, dp, command):
"""Test whether a command handler responds to its command
and not to others, or badly formatted commands"""
handler = self.make_default_handler()
dp.add_handler(handler)
message.text = '/test'
dp.process_update(Update(0, message))
assert self.test_flag
assert self.response(dp, make_command_update(command))
assert not is_match(handler, make_command_update(command[1:]))
assert not is_match(handler, make_command_update('/not{}'.format(command[1:])))
assert not is_match(handler, make_command_update('not {} at start'.format(command)))
message.text = '/nottest'
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.text = 'test'
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.text = 'not /test at start'
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.entities = []
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is None or check is False
@pytest.mark.parametrize('command',
@pytest.mark.parametrize('cmd',
['way_too_longcommand1234567yes_way_toooooooLong', 'ïñválídletters',
'invalid #&* chars'],
ids=['too long', 'invalid letter', 'invalid characters'])
def test_invalid_commands(self, command):
def test_invalid_commands(self, cmd):
with pytest.raises(ValueError, match='not a valid bot command'):
CommandHandler(command, self.callback_basic)
CommandHandler(cmd, self.callback_basic)
def test_command_list(self, message):
def test_command_list(self):
"""A command handler with multiple commands registered should respond to all of them."""
handler = CommandHandler(['test', 'star'], self.callback_basic)
message.text = '/test'
check = handler.check_update(Update(0, message))
message.text = '/star'
check = handler.check_update(Update(0, message))
message.text = '/stop'
check = handler.check_update(Update(0, message))
assert check is None or check is False
assert is_match(handler, make_command_update('/test'))
assert is_match(handler, make_command_update('/star'))
assert not is_match(handler, make_command_update('/stop'))
def test_deprecation_warning(self):
"""``allow_edited`` deprecated in favor of filters"""
with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'):
CommandHandler('test', self.callback_basic, allow_edited=True)
self.make_default_handler(allow_edited=True)
def test_no_edited(self, message):
handler = CommandHandler('test', self.callback_basic)
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
def test_edited(self, command_message):
"""Test that a CH responds to an edited message iff its filters allow it"""
handler_edited = self.make_default_handler()
handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message)
self._test_edited(command_message, handler_edited, handler_no_edited)
check = handler.check_update(Update(0, edited_message=message))
assert check is not None and check is not False
def test_edited_deprecated(self, command_message):
"""Test that a CH responds to an edited message iff ``allow_edited`` is True"""
handler_edited = self.make_default_handler(allow_edited=True)
handler_no_edited = self.make_default_handler(allow_edited=False)
self._test_edited(command_message, handler_edited, handler_no_edited)
handler = CommandHandler('test', self.callback_basic,
filters=~Filters.update.edited_message)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
def test_directed_commands(self, bot, command):
"""Test recognition of commands with a mention to the bot"""
handler = self.make_default_handler()
assert is_match(handler, make_command_update(command + '@' + bot.username, bot=bot))
assert not is_match(handler, make_command_update(command + '@otherbot', bot=bot))
check = handler.check_update(Update(0, edited_message=message))
assert check is None or check is False
def test_with_filter(self, command):
"""Test that a CH with a (generic) filter responds iff its filters match"""
handler = self.make_default_handler(filters=Filters.group)
assert is_match(handler, make_command_update(command, chat=Chat(-23, Chat.GROUP)))
assert not is_match(handler, make_command_update(command, chat=Chat(23, Chat.PRIVATE)))
def test_edited_deprecated(self, message):
handler = CommandHandler('test', self.callback_basic,
allow_edited=False)
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is None or check is False
handler = CommandHandler('test', self.callback_basic,
allow_edited=True)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is not None and check is not False
def test_directed_commands(self, message):
handler = CommandHandler('test', self.callback_basic)
message.text = '/test@{}'.format(message.bot.username)
message.entities[0].length = len(message.text)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
message.text = '/test@otherbot'
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_with_filter(self, message):
handler = CommandHandler('test', self.callback_basic, Filters.group)
message.chat = Chat(-23, 'group')
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
message.chat = Chat(23, 'private')
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_pass_args(self, dp, message):
handler = CommandHandler('test', self.ch_callback_args, pass_args=True)
def test_pass_args(self, dp, bot, command):
"""Test the passing of arguments alongside a command"""
handler = self.make_default_handler(self.ch_callback_args, pass_args=True)
dp.add_handler(handler)
at_command = '{}@{}'.format(command, bot.username)
assert self.response(dp, make_command_update(command))
assert self.response(dp, make_command_update(command + ' one two'))
assert self.response(dp, make_command_update(at_command, bot=bot))
assert self.response(dp, make_command_update(at_command + ' one two', bot=bot))
message.text = '/test'
dp.process_update(Update(0, message=message))
assert self.test_flag
self.test_flag = False
message.text = '/test@{}'.format(message.bot.username)
message.entities[0].length = len(message.text)
dp.process_update(Update(0, message=message))
assert self.test_flag
self.test_flag = False
message.text = '/test@{} one two'.format(message.bot.username)
dp.process_update(Update(0, message=message))
assert self.test_flag
self.test_flag = False
message.text = '/test one two'
message.entities[0].length = len('/test')
dp.process_update(Update(0, message=message))
assert self.test_flag
def test_newline(self, dp, message):
handler = CommandHandler('test', self.callback_basic)
def test_newline(self, dp, command):
"""Assert that newlines don't interfere with a command handler matching a message"""
handler = self.make_default_handler()
dp.add_handler(handler)
update = make_command_update(command + '\nfoobar')
assert is_match(handler, update)
assert self.response(dp, update)
message.text = '/test\nfoobar'
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
dp.process_update(Update(0, message))
assert self.test_flag
def test_pass_user_or_chat_data(self, dp, message):
handler = CommandHandler('test', self.callback_data_1,
pass_user_data=True)
@pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS)
def test_pass_data(self, dp, command_update, pass_combination, pass_keyword):
handler = CommandHandler('test', self.make_callback_for(pass_keyword), **pass_combination)
dp.add_handler(handler)
message.text = '/test'
dp.process_update(Update(0, message=message))
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
self.test_flag = False
dp.process_update(Update(0, message=message))
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_data_2,
pass_chat_data=True,
pass_user_data=True)
dp.add_handler(handler)
self.test_flag = False
dp.process_update(Update(0, message=message))
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, message):
handler = CommandHandler('test', self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
message.text = '/test'
dp.process_update(Update(0, message=message))
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
dp.process_update(Update(0, message=message))
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_queue_2,
pass_job_queue=True,
pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
dp.process_update(Update(0, message=message))
assert self.test_flag
assert self.response(dp, command_update) == pass_combination.get(pass_keyword, False)
def test_other_update_types(self, false_update):
handler = CommandHandler('test', self.callback_basic)
check = handler.check_update(false_update)
assert check is None or check is False
"""Test that a command handler doesn't respond to unrelated updates"""
handler = self.make_default_handler()
assert not is_match(handler, false_update)
def test_filters_for_wrong_command(self, message):
def test_filters_for_wrong_command(self, mock_filter):
"""Filters should not be executed if the command does not match the handler"""
handler = self.make_default_handler(filters=mock_filter)
assert not is_match(handler, make_command_update('/star'))
assert not mock_filter.tested
class TestFilter(BaseFilter):
def __init__(self):
self.tested = False
def filter(self, message):
self.tested = True
test_filter = TestFilter()
handler = CommandHandler('test', self.callback_basic,
filters=test_filter)
message.text = '/star'
check = handler.check_update(Update(0, message=message))
assert check is None or check is False
assert not test_filter.tested
def test_context(self, cdp, message):
handler = CommandHandler('test', self.callback_context)
def test_context(self, cdp, command_update):
"""Test correct behaviour of CHs with context-based callbacks"""
handler = self.make_default_handler(self.callback_context)
cdp.add_handler(handler)
assert self.response(cdp, command_update)
message.text = '/test'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_args(self, cdp, command):
"""Test CHs that pass arguments through ``context``"""
handler = self.make_default_handler(self.callback_context_args)
self._test_context_args_or_regex(cdp, handler, command)
def test_context_args(self, cdp, message):
handler = CommandHandler('test', self.callback_context_args)
cdp.add_handler(handler)
def test_context_regex(self, cdp, command):
"""Test CHs with context-based callbacks and a single filter"""
handler = self.make_default_handler(self.callback_context_regex1,
filters=Filters.regex('one two'))
self._test_context_args_or_regex(cdp, handler, command)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text = '/test one two'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_regex(self, cdp, message):
handler = CommandHandler('test', self.callback_context_regex1, Filters.regex('one two'))
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text += ' one two'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_multiple_regex(self, cdp, message):
handler = CommandHandler('test', self.callback_context_regex2,
Filters.regex('one') & Filters.regex('two'))
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text += ' one two'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_multiple_regex(self, cdp, command):
"""Test CHs with context-based callbacks and filters combined"""
handler = self.make_default_handler(self.callback_context_regex2,
filters=Filters.regex('one') & Filters.regex('two'))
self._test_context_args_or_regex(cdp, handler, command)
par = ['!help', '!test', '#help', '#test', 'mytrig-help', 'mytrig-test']
# ----------------------------- PrefixHandler -----------------------------
def combinations(prefixes, commands):
return (prefix + command for prefix in prefixes for command in commands)
@pytest.fixture(scope='function', params=par)
def prefixmessage(bot, request):
return Message(message_id=1,
from_user=User(id=1, first_name='', is_bot=False),
date=None,
chat=Chat(id=1, type=''),
text=request.param,
bot=bot)
class TestPrefixHandler(BaseTest):
# Prefixes and commands with which to test PrefixHandler:
PREFIXES = ['!', '#', 'mytrig-']
COMMANDS = ['help', 'test']
COMBINATIONS = list(combinations(PREFIXES, COMMANDS))
@pytest.fixture(scope='class', params=PREFIXES)
def prefix(self, request):
return request.param
class TestPrefixHandler(object):
test_flag = False
SRE_TYPE = type(re.match("", ""))
@pytest.fixture(scope='class', params=[1, 2], ids=['single prefix', 'multiple prefixes'])
def prefixes(self, request):
return TestPrefixHandler.PREFIXES[:request.param]
@pytest.fixture(autouse=True)
def reset(self):
self.test_flag = False
@pytest.fixture(scope='class', params=COMMANDS)
def command(self, request):
return request.param
def callback_basic(self, bot, update):
test_bot = isinstance(bot, Bot)
test_update = isinstance(update, Update)
self.test_flag = test_bot and test_update
@pytest.fixture(scope='class', params=[1, 2], ids=['single command', 'multiple commands'])
def commands(self, request):
return TestPrefixHandler.COMMANDS[:request.param]
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)
@pytest.fixture(scope='class')
def prefix_message_text(self, prefix, command):
return prefix + command
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)
@pytest.fixture(scope='class')
def prefix_message(self, prefix_message_text):
return make_message(prefix_message_text)
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)
@pytest.fixture(scope='class')
def prefix_message_update(self, prefix_message):
return make_message_update(prefix_message)
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 make_default_handler(self, callback=None, **kwargs):
callback = callback or self.callback_basic
return PrefixHandler(self.PREFIXES, self.COMMANDS, callback, **kwargs)
def ch_callback_args(self, bot, update, args):
if update.message.text in par:
if update.message.text in TestPrefixHandler.COMBINATIONS:
self.test_flag = len(args) == 0
else:
self.test_flag = args == ['one', 'two']
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 isinstance(context.chat_data, dict)
and isinstance(update.message, Message))
def callback_context_args(self, update, context):
self.test_flag = context.args == ['one', 'two']
def callback_context_regex1(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 1
self.test_flag = types and num
def callback_context_regex2(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 2
self.test_flag = types and num
def test_basic(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
def test_basic(self, dp, prefix, command):
"""Test the basic expected response from a prefix handler"""
handler = self.make_default_handler()
dp.add_handler(handler)
text = prefix + command
dp.process_update(Update(0, prefixmessage))
assert self.test_flag
assert self.response(dp, make_message_update(text))
assert not is_match(handler, make_message_update(command))
assert not is_match(handler, make_message_update(prefix + 'notacommand'))
assert not is_match(handler, make_command_update('not {} at start'.format(text)))
prefixmessage.text = 'test'
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
def test_single_multi_prefixes_commands(self, prefixes, commands, prefix_message_update):
"""Test various combinations of prefixes and commands"""
handler = self.make_default_handler()
result = is_match(handler, prefix_message_update)
expected = prefix_message_update.message.text in combinations(prefixes, commands)
return result == expected
prefixmessage.text = '#nocom'
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
def test_edited(self, prefix_message):
handler_edited = self.make_default_handler()
handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message)
self._test_edited(prefix_message, handler_edited, handler_no_edited)
message.text = 'not !test at start'
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_with_filter(self, prefix_message_text):
handler = self.make_default_handler(filters=Filters.group)
text = prefix_message_text
assert is_match(handler, make_message_update(text, chat=Chat(-23, Chat.GROUP)))
assert not is_match(handler, make_message_update(text, chat=Chat(23, Chat.PRIVATE)))
def test_single_prefix_single_command(self, prefixmessage):
handler = PrefixHandler('!', 'test', self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_single_prefix_multi_command(self, prefixmessage):
handler = PrefixHandler('!', ['test', 'help'], self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test', '!help']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_multi_prefix_single_command(self, prefixmessage):
handler = PrefixHandler(['!', '#'], 'test', self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test', '#test']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_no_edited(self, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=prefixmessage))
assert check is not None and check is not False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=~Filters.update.edited_message)
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=prefixmessage))
assert check is None or check is False
def test_with_filter(self, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=Filters.group)
prefixmessage.chat = Chat(-23, 'group')
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
prefixmessage.chat = Chat(23, 'private')
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
def test_pass_args(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.ch_callback_args,
pass_args=True)
def test_pass_args(self, dp, prefix_message):
handler = self.make_default_handler(self.ch_callback_args, pass_args=True)
dp.add_handler(handler)
assert self.response(dp, make_message_update(prefix_message))
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
update_with_args = make_message_update(prefix_message.text + ' one two')
assert self.response(dp, update_with_args)
self.test_flag = False
prefixmessage.text += ' one two'
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
def test_pass_user_or_chat_data(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
pass_user_data=True)
@pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS)
def test_pass_data(self, dp, pass_combination, prefix_message_update, pass_keyword):
"""Assert that callbacks receive data iff its corresponding ``pass_*`` kwarg is enabled"""
handler = self.make_default_handler(self.make_callback_for(pass_keyword),
**pass_combination)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_2,
pass_chat_data=True, pass_user_data=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_2,
pass_job_queue=True, pass_update_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
assert self.response(dp, prefix_message_update) \
== pass_combination.get(pass_keyword, False)
def test_other_update_types(self, false_update):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
check = handler.check_update(false_update)
assert check is None or check is False
handler = self.make_default_handler()
assert not is_match(handler, false_update)
def test_filters_for_wrong_command(self, prefixmessage):
def test_filters_for_wrong_command(self, mock_filter):
"""Filters should not be executed if the command does not match the handler"""
handler = self.make_default_handler(filters=mock_filter)
assert not is_match(handler, make_message_update('/test'))
assert not mock_filter.tested
class TestFilter(BaseFilter):
def __init__(self):
self.tested = False
def filter(self, message):
self.tested = True
test_filter = TestFilter()
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=test_filter)
prefixmessage.text = '/star'
check = handler.check_update(Update(0, message=prefixmessage))
assert check is None or check is False
assert not test_filter.tested
def test_context(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_context)
def test_context(self, cdp, prefix_message_update):
handler = self.make_default_handler(self.callback_context)
cdp.add_handler(handler)
assert self.response(cdp, prefix_message_update)
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_args(self, cdp, prefix_message_text):
handler = self.make_default_handler(self.callback_context_args)
self._test_context_args_or_regex(cdp, handler, prefix_message_text)
def test_context_args(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_args)
cdp.add_handler(handler)
def test_context_regex(self, cdp, prefix_message_text):
handler = self.make_default_handler(self.callback_context_regex1,
filters=Filters.regex('one two'))
self._test_context_args_or_regex(cdp, handler, prefix_message_text)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_regex(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_regex1, Filters.regex('one two'))
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_multiple_regex(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_regex2,
Filters.regex('one') & Filters.regex('two'))
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_multiple_regex(self, cdp, prefix_message_text):
handler = self.make_default_handler(self.callback_context_regex2,
filters=Filters.regex('one') & Filters.regex(
'two'))
self._test_context_args_or_regex(cdp, handler, prefix_message_text)

View file

@ -43,6 +43,10 @@ class TestConversationHandler(object):
# and then we can start coding!
END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4)
# Drinking state definitions (nested)
# At first we're holding the cup. Then we sip coffee, and last we swallow it
HOLDING, SIPPING, SWALLOWING, REPLENISHING, STOPPING = map(chr, range(ord('a'), ord('f')))
current_state, entry_points, states, fallbacks = None, None, None, None
group = Chat(0, Chat.GROUP)
second_group = Chat(1, Chat.GROUP)
@ -69,6 +73,43 @@ class TestConversationHandler(object):
self.fallbacks = [CommandHandler('eat', self.start)]
self.is_timeout = False
# for nesting tests
self.nested_states = {
self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)],
self.BREWING: [CommandHandler('pourCoffee', self.drink)],
self.CODING: [
CommandHandler('keepCoding', self.code),
CommandHandler('gettingThirsty', self.start),
CommandHandler('drinkMore', self.drink)
],
}
self.drinking_entry_points = [CommandHandler('hold', self.hold)]
self.drinking_states = {
self.HOLDING: [CommandHandler('sip', self.sip)],
self.SIPPING: [CommandHandler('swallow', self.swallow)],
self.SWALLOWING: [CommandHandler('hold', self.hold)]
}
self.drinking_fallbacks = [CommandHandler('replenish', self.replenish),
CommandHandler('stop', self.stop),
CommandHandler('end', self.end),
CommandHandler('startCoding', self.code),
CommandHandler('drinkMore', self.drink)]
self.drinking_entry_points.extend(self.drinking_fallbacks)
# Map nested states to parent states:
self.drinking_map_to_parent = {
# Option 1 - Map a fictional internal state to an external parent state
self.REPLENISHING: self.BREWING,
# Option 2 - Map a fictional internal state to the END state on the parent
self.STOPPING: self.END,
# Option 3 - Map the internal END state to an external parent state
self.END: self.CODING,
# Option 4 - Map an external state to the same external parent state
self.CODING: self.CODING,
# Option 5 - Map an external state to the internal entry point
self.DRINKING: self.DRINKING
}
# State handlers
def _set_state(self, update, state):
self.current_state[update.message.from_user.id] = state
@ -103,6 +144,23 @@ class TestConversationHandler(object):
def passout2(self, bot, update):
self.is_timeout = True
# Drinking actions (nested)
def hold(self, bot, update):
return self._set_state(update, self.HOLDING)
def sip(self, bot, update):
return self._set_state(update, self.SIPPING)
def swallow(self, bot, update):
return self._set_state(update, self.SWALLOWING)
def replenish(self, bot, update):
return self._set_state(update, self.REPLENISHING)
def stop(self, bot, update):
return self._set_state(update, self.STOPPING)
# Tests
def test_per_all_false(self):
with pytest.raises(ValueError, match="can't all be 'False'"):
@ -609,3 +667,108 @@ class TestConversationHandler(object):
"If 'per_chat=True', 'InlineQueryHandler' can not be used,"
" since inline queries have no chat context."
)
def test_nested_conversation_handler(self, dp, bot, user1, user2):
self.nested_states[self.DRINKING] = [ConversationHandler(
entry_points=self.drinking_entry_points,
states=self.drinking_states,
fallbacks=self.drinking_fallbacks,
map_to_parent=self.drinking_map_to_parent)]
handler = ConversationHandler(entry_points=self.entry_points,
states=self.nested_states,
fallbacks=self.fallbacks)
dp.add_handler(handler)
# User one, starts the state machine.
message = Message(0, user1, None, self.group, text='/start', bot=bot,
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))])
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.THIRSTY
# The user is thirsty and wants to brew coffee.
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.BREWING
# Lets pour some coffee.
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.DRINKING
# The user is holding the cup
message.text = '/hold'
message.entities[0].length = len('/hold')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.HOLDING
# The user is sipping coffee
message.text = '/sip'
message.entities[0].length = len('/sip')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.SIPPING
# The user is swallowing
message.text = '/swallow'
message.entities[0].length = len('/swallow')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.SWALLOWING
# The user is holding the cup again
message.text = '/hold'
message.entities[0].length = len('/hold')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.HOLDING
# The user wants to replenish the coffee supply
message.text = '/replenish'
message.entities[0].length = len('/replenish')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.REPLENISHING
assert handler.conversations[(0, user1.id)] == self.BREWING
# The user wants to drink their coffee again
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.DRINKING
# The user is now ready to start coding
message.text = '/startCoding'
message.entities[0].length = len('/startCoding')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.CODING
# The user decides it's time to drink again
message.text = '/drinkMore'
message.entities[0].length = len('/drinkMore')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.DRINKING
# The user is holding their cup
message.text = '/hold'
message.entities[0].length = len('/hold')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.HOLDING
# The user wants to end with the drinking and go back to coding
message.text = '/end'
message.entities[0].length = len('/end')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.END
assert handler.conversations[(0, user1.id)] == self.CODING
# The user wants to drink once more
message.text = '/drinkMore'
message.entities[0].length = len('/drinkMore')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.DRINKING
# The user wants to stop altogether
message.text = '/stop'
message.entities[0].length = len('/stop')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.STOPPING
assert handler.conversations.get((0, user1.id)) is None

View file

@ -24,10 +24,12 @@ from time import sleep
import pytest
from telegram import TelegramError, Message, User, Chat, Update, Bot, MessageEntity
from telegram.ext import MessageHandler, Filters, CommandHandler, CallbackContext, JobQueue
from telegram.ext import (MessageHandler, Filters, CommandHandler, CallbackContext,
JobQueue, BasePersistence)
from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop
from telegram.utils.deprecate import TelegramDeprecationWarning
from tests.conftest import create_dp
from collections import defaultdict
@pytest.fixture(scope='function')
@ -276,10 +278,11 @@ class TestDispatcher(object):
def test_exception_in_handler(self, dp, bot):
passed = []
err = Exception('General exception')
def start1(b, u):
passed.append('start1')
raise Exception('General exception')
raise err
def start2(b, u):
passed.append('start2')
@ -298,14 +301,14 @@ class TestDispatcher(object):
bot=bot))
# If an unhandled exception was caught, no further handlers from the same group should be
# called.
# called. Also, the error handler should be called and receive the exception
passed = []
dp.add_handler(CommandHandler('start', start1), 1)
dp.add_handler(CommandHandler('start', start2), 1)
dp.add_handler(CommandHandler('start', start3), 2)
dp.add_error_handler(error)
dp.process_update(update)
assert passed == ['start1', 'start3']
assert passed == ['start1', 'error', err, 'start3']
def test_telegram_error_in_handler(self, dp, bot):
passed = []
@ -341,6 +344,49 @@ class TestDispatcher(object):
assert passed == ['start1', 'error', err, 'start3']
assert passed[2] is err
def test_error_while_saving_chat_data(self, dp, bot):
increment = []
class OwnPersistence(BasePersistence):
def __init__(self):
super(BasePersistence, self).__init__()
self.store_user_data = True
self.store_chat_data = True
def get_chat_data(self):
return defaultdict(dict)
def update_chat_data(self, chat_id, data):
raise Exception
def get_user_data(self):
return defaultdict(dict)
def update_user_data(self, user_id, data):
raise Exception
def start1(b, u):
pass
def error(b, u, e):
increment.append("error")
# If updating a user_data or chat_data from a persistence object throws an error,
# the error handler should catch it
update = Update(1, message=Message(1, User(1, "Test", False), None, Chat(1, "lala"),
text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/start'))],
bot=bot))
my_persistence = OwnPersistence()
dp = Dispatcher(bot, None, persistence=my_persistence)
dp.add_handler(CommandHandler('start', start1))
dp.add_error_handler(error)
dp.process_update(update)
assert increment == ["error", "error"]
def test_flow_stop_in_error_handler(self, dp, bot):
passed = []
err = TelegramError('Telegram error')

View file

@ -16,6 +16,7 @@
#
# 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 Sticker
from telegram import Update
@ -31,6 +32,36 @@ class TestHelpers(object):
assert expected_str == helpers.escape_markdown(test_str)
def test_create_deep_linked_url(self):
username = 'JamesTheMock'
payload = "hello"
expected = "https://t.me/{}?start={}".format(username, payload)
actual = helpers.create_deep_linked_url(username, payload)
assert expected == actual
expected = "https://t.me/{}?startgroup={}".format(username, payload)
actual = helpers.create_deep_linked_url(username, payload, group=True)
assert expected == actual
payload = ""
expected = "https://t.me/{}".format(username)
assert expected == helpers.create_deep_linked_url(username)
assert expected == helpers.create_deep_linked_url(username, payload)
payload = None
assert expected == helpers.create_deep_linked_url(username, payload)
with pytest.raises(ValueError):
helpers.create_deep_linked_url(username, 'text with spaces')
with pytest.raises(ValueError):
helpers.create_deep_linked_url(username, '0' * 65)
with pytest.raises(ValueError):
helpers.create_deep_linked_url(None, None)
with pytest.raises(ValueError): # too short username (4 is minimum)
helpers.create_deep_linked_url("abc", None)
def test_effective_message_type(self):
def build_test_message(**kwargs):
@ -47,7 +78,7 @@ class TestHelpers(object):
assert helpers.effective_message_type(test_message) == 'text'
test_message.text = None
test_message = build_test_message(sticker=Sticker('sticker_id', 50, 50))
test_message = build_test_message(sticker=Sticker('sticker_id', 50, 50, False))
assert helpers.effective_message_type(test_message) == 'sticker'
test_message.sticker = None

View file

@ -57,7 +57,7 @@ def message(bot):
[PhotoSize('game_photo_id', 30, 30), ])},
{'photo': [PhotoSize('photo_id', 50, 50)],
'caption': 'photo_file'},
{'sticker': Sticker('sticker_id', 50, 50)},
{'sticker': Sticker('sticker_id', 50, 50, True)},
{'video': Video('video_id', 12, 12, 12),
'caption': 'video_file'},
{'voice': Voice('voice_id', 5)},

View file

@ -16,9 +16,8 @@
#
# 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 os
import inspect
import sys
from platform import python_implementation
import certifi
import pytest
@ -149,7 +148,7 @@ for thing in soup.select('h4 > a.anchor'):
@pytest.mark.parametrize(('method', 'data'), argvalues=argvalues, ids=names)
@pytest.mark.skipif(not sys.version_info >= (3, 7) or python_implementation() != 'CPython',
reason='follow_wrapped (inspect.signature) is not supported on this platform')
@pytest.mark.skipif(os.getenv('TEST_OFFICIAL') != 'true',
reason='test_official is not enabled')
def test_official(method, data):
method(data)

View file

@ -176,10 +176,10 @@ class TestBasePersistence(object):
with caplog.at_level(logging.ERROR):
dp.process_update(u)
rec = caplog.records[-1]
assert rec.msg == 'Saving user data raised an error'
assert rec.msg == 'No error handlers are registered, logging exception.'
assert rec.levelname == 'ERROR'
rec = caplog.records[-2]
assert rec.msg == 'Saving chat data raised an error'
assert rec.msg == 'No error handlers are registered, logging exception.'
assert rec.levelname == 'ERROR'
m.from_user = user2
m.chat = chat1
@ -224,7 +224,7 @@ def pickle_persistence():
return PicklePersistence(filename='pickletest',
store_user_data=True,
store_chat_data=True,
singe_file=False,
single_file=False,
on_flush=False)
@ -233,7 +233,7 @@ def pickle_persistence_only_chat():
return PicklePersistence(filename='pickletest',
store_user_data=False,
store_chat_data=True,
singe_file=False,
single_file=False,
on_flush=False)
@ -242,7 +242,7 @@ def pickle_persistence_only_user():
return PicklePersistence(filename='pickletest',
store_user_data=True,
store_chat_data=False,
singe_file=False,
single_file=False,
on_flush=False)
@ -552,7 +552,7 @@ class TestPickelPersistence(object):
pickle_persistence_2 = PicklePersistence(filename='pickletest',
store_user_data=True,
store_chat_data=True,
singe_file=False,
single_file=False,
on_flush=False)
u = Updater(bot=bot, persistence=pickle_persistence_2)
dp = u.dispatcher
@ -572,7 +572,7 @@ class TestPickelPersistence(object):
pickle_persistence_2 = PicklePersistence(filename='pickletest',
store_user_data=True,
store_chat_data=True,
singe_file=False,
single_file=False,
on_flush=False)
assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!'
assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!'
@ -592,7 +592,7 @@ class TestPickelPersistence(object):
pickle_persistence_2 = PicklePersistence(filename='pickletest',
store_user_data=False,
store_chat_data=True,
singe_file=False,
single_file=False,
on_flush=False)
assert pickle_persistence_2.get_user_data() == {}
assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!'
@ -611,7 +611,7 @@ class TestPickelPersistence(object):
pickle_persistence_2 = PicklePersistence(filename='pickletest',
store_user_data=True,
store_chat_data=False,
singe_file=False,
single_file=False,
on_flush=False)
assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!'
assert pickle_persistence_2.get_chat_data()[-4242424242] == {}

View file

@ -360,7 +360,7 @@ class TestPhoto(object):
b = PhotoSize(photo.file_id, self.width, self.height)
c = PhotoSize(photo.file_id, 0, 0)
d = PhotoSize('', self.width, self.height)
e = Sticker(photo.file_id, self.width, self.height)
e = Sticker(photo.file_id, self.width, self.height, False)
assert a == b
assert hash(a) == hash(b)

View file

@ -49,6 +49,7 @@ class TestSticker(object):
emoji = '💪'
width = 510
height = 512
is_animated = False
file_size = 39518
thumb_width = 319
thumb_height = 320
@ -66,6 +67,7 @@ class TestSticker(object):
def test_expected_values(self, sticker):
assert sticker.width == self.width
assert sticker.height == self.height
assert sticker.is_animated == self.is_animated
assert sticker.file_size == self.file_size
assert sticker.thumb.width == self.thumb_width
assert sticker.thumb.height == self.thumb_height
@ -81,6 +83,7 @@ class TestSticker(object):
assert message.sticker.file_id != ''
assert message.sticker.width == sticker.width
assert message.sticker.height == sticker.height
assert message.sticker.is_animated == sticker.is_animated
assert message.sticker.file_size == sticker.file_size
assert isinstance(message.sticker.thumb, PhotoSize)
@ -132,6 +135,7 @@ class TestSticker(object):
assert message.sticker.file_id != ''
assert message.sticker.width == sticker.width
assert message.sticker.height == sticker.height
assert message.sticker.is_animated == sticker.is_animated
assert message.sticker.file_size == sticker.file_size
assert isinstance(message.sticker.thumb, PhotoSize)
@ -146,6 +150,7 @@ class TestSticker(object):
'file_id': 'not a file id',
'width': self.width,
'height': self.height,
'is_animated': self.is_animated,
'thumb': sticker.thumb.to_dict(),
'emoji': self.emoji,
'file_size': self.file_size
@ -155,6 +160,7 @@ class TestSticker(object):
assert json_sticker.file_id == 'not a file id'
assert json_sticker.width == self.width
assert json_sticker.height == self.height
assert json_sticker.is_animated == self.is_animated
assert json_sticker.emoji == self.emoji
assert json_sticker.file_size == self.file_size
assert json_sticker.thumb == sticker.thumb
@ -174,6 +180,7 @@ class TestSticker(object):
assert sticker_dict['file_id'] == sticker.file_id
assert sticker_dict['width'] == sticker.width
assert sticker_dict['height'] == sticker.height
assert sticker_dict['is_animated'] == sticker.is_animated
assert sticker_dict['file_size'] == sticker.file_size
assert sticker_dict['thumb'] == sticker.thumb.to_dict()
@ -194,11 +201,11 @@ class TestSticker(object):
bot.send_sticker(chat_id)
def test_equality(self, sticker):
a = Sticker(sticker.file_id, self.width, self.height)
b = Sticker(sticker.file_id, self.width, self.height)
c = Sticker(sticker.file_id, 0, 0)
d = Sticker('', self.width, self.height)
e = PhotoSize(sticker.file_id, self.width, self.height)
a = Sticker(sticker.file_id, self.width, self.height, self.is_animated)
b = Sticker(sticker.file_id, self.width, self.height, self.is_animated)
c = Sticker(sticker.file_id, 0, 0, False)
d = Sticker('', self.width, self.height, self.is_animated)
e = PhotoSize(sticker.file_id, self.width, self.height, self.is_animated)
assert a == b
assert hash(a) == hash(b)
@ -224,8 +231,9 @@ def sticker_set(bot):
class TestStickerSet(object):
title = 'Test stickers'
is_animated = True
contains_masks = False
stickers = [Sticker('file_id', 512, 512)]
stickers = [Sticker('file_id', 512, 512, True)]
name = 'NOTAREALNAME'
def test_de_json(self, bot):
@ -233,6 +241,7 @@ class TestStickerSet(object):
json_dict = {
'name': name,
'title': self.title,
'is_animated': self.is_animated,
'contains_masks': self.contains_masks,
'stickers': [x.to_dict() for x in self.stickers]
}
@ -240,6 +249,7 @@ class TestStickerSet(object):
assert sticker_set.name == name
assert sticker_set.title == self.title
assert sticker_set.is_animated == self.is_animated
assert sticker_set.contains_masks == self.contains_masks
assert sticker_set.stickers == self.stickers
@ -258,6 +268,7 @@ class TestStickerSet(object):
assert isinstance(sticker_set_dict, dict)
assert sticker_set_dict['name'] == sticker_set.name
assert sticker_set_dict['title'] == sticker_set.title
assert sticker_set_dict['is_animated'] == sticker_set.is_animated
assert sticker_set_dict['contains_masks'] == sticker_set.contains_masks
assert sticker_set_dict['stickers'][0] == sticker_set.stickers[0].to_dict()
@ -282,10 +293,10 @@ class TestStickerSet(object):
assert sticker.get_file()
def test_equality(self):
a = StickerSet(self.name, self.title, self.contains_masks, self.stickers)
b = StickerSet(self.name, self.title, self.contains_masks, self.stickers)
c = StickerSet(self.name, None, None, None)
d = StickerSet('blah', self.title, self.contains_masks, self.stickers)
a = StickerSet(self.name, self.title, self.is_animated, self.contains_masks, self.stickers)
b = StickerSet(self.name, self.title, self.is_animated, self.contains_masks, self.stickers)
c = StickerSet(self.name, None, None, None, None)
d = StickerSet('blah', self.title, self.is_animated, self.contains_masks, self.stickers)
e = Audio(self.name, 0, None, None)
assert a == b