Compare commits

..

19 Commits

Author SHA1 Message Date
Hinrich Mahler 38a33581b1 Bump version to v12.6 2020-04-10 23:52:08 +02:00
Hinrich Mahler fe821c08e6 Doc Fixes 2020-04-10 23:43:58 +02:00
Harshil 0a9f4bfbdd Doc fixes (#1884)
* Bot.py doc fixes

All docs obtained from official Bot API docs

* made flake8 happy

* address review

Also improved consistency of `returns:` in docs

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-10 20:05:01 +02:00
Bibo-Joshi c4364c7166 GitHub Actions: Use checkout@v2 (#1887) 2020-04-10 19:57:52 +02:00
Bibo-Joshi d63e710784 API 4.7 (#1858)
* Pure API changes

* Address review

* set Bot.commands on successfull call of set_my_commands

* Get started on tests

* More tests!

* More Coverage!

* Reset changes in utils.request

* Filters.dice, Filters.dice.text

* more coverage

* Address review

* Address review

* Test stop_poll with reply_markup

* Test stop_poll also without reply_markup

* Rephrase note on 'dice'

* Fix grammar in note on Filters.dice

* update api version readme

* address review
2020-04-10 19:22:45 +02:00
Bibo-Joshi f379f54d5a Tweak persistence handling (#1827)
* Unify persistence updates in dispatcher

* Ensure user/chat_data is not None when updating it

* Update persistence after job runs

* Increase coverage
2020-04-10 13:23:13 +02:00
Bibo-Joshi bdf0cb91f3 Pass last valid context to TIMEOUT handlers (#1826) 2020-04-10 13:18:43 +02:00
Bibo-Joshi 3101ea8432 Favor concrete types over "Iterable" (#1882)
* Use concrete types instead of 'iterable'

* Fix overlooked docstring

* address review
2020-04-08 22:49:01 +02:00
Harshil beb8ba3db0 Doc Fixes (#1874)
* doc fixes

* Update AUTHORS.rst

* More doc fixes

All docs were obtained from official Bot API docs.

* Shortened line length

Did this so it passes codacy check

* Revert id docstring changes

* typo

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-07 17:25:17 +02:00
Bibo-Joshi f0b1aeb6fd Customize issue template chooser (#1880)
* Customize issue template chooser

* Improve wording
2020-04-07 15:45:17 +02:00
Bibo-Joshi d65558888e Add note on UTC to run_{repeating, once} (#1854) 2020-03-31 00:05:08 +02:00
Bibo-Joshi 61a66a32c8 Add tests for empty string as switch_inline_query(_current_chat) (#1635) 2020-03-31 00:03:45 +02:00
Hinrich Mahler 392d4e1a9c Bump version to v12.5.1 2020-03-30 18:25:53 +02:00
Andrej730 9cb34af65a Fix UTC as default tzinfo for Jobs (#1696)
1. Made sure that default tzinfo in JobQueue is UTC #1693.
2. Added test that checks that all methods by default set job.tzinfo as UTC.
2020-03-30 18:10:27 +02:00
Bibo-Joshi e9cb6675ca PrefixHandlers command and prefix editable (#1636)
* Rename internal list of PrefixHandler

* Make PFH.prefix and .command setable attributes

* Improve coverage
2020-03-30 17:49:50 +02:00
Bibo-Joshi 982f6707e1 Make ConversationHandler attributes immutable (#1756)
* Make ConversationHandler attributes immutable

* Add forgotten name property to test_immutable
2020-03-30 17:37:37 +02:00
Rys Artem d55d981e22 Reorder tests to make them more stable (#1835) 2020-03-30 17:06:24 +02:00
Iulian Onofrei f20953f7a9 Fix docs wording (#1855) 2020-03-30 00:32:06 +03:00
Bibo-Joshi e18220be10 Add docs for PollHandler and PollAnswerHandler (#1853) 2020-03-29 11:24:44 +02:00
57 changed files with 1518 additions and 393 deletions
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Group
url: https://telegram.me/pythontelegrambotgroup
about: Questions asked on the group usually get answered faster.
- name: IRC Channel
url: https://webchat.freenode.net/?channels=##python-telegram-bot
about: In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
test-build: True
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
@@ -73,7 +73,7 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
@@ -102,7 +102,7 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
+1
View File
@@ -37,6 +37,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `evgfilim1 <https://github.com/evgfilim1>`_
- `franciscod <https://github.com/franciscod>`_
- `gamgi <https://github.com/gamgi>`_
- `Harshil <https://github.com/harshil21>`_
- `Hugo Damer <https://github.com/HakimusGIT>`_
- `ihoru <https://github.com/ihoru>`_
- `Jasmin Bom <https://github.com/jsmnbom>`_
+49
View File
@@ -2,6 +2,55 @@
Changelog
=========
Version 12.6
============
*Released 2020-04-10*
**Major Changes:**
- Bot API 4.7 support. **Note:** In ``Bot.create_new_sticker_set`` and ``Bot.add_sticker_to_set``, the order of the parameters had be changed, as the ``png_sticker`` parameter is now optional. (`#1858`_)
**Minor changes, CI improvements or bug fixes:**
- Add tests for ``swtich_inline_query(_current_chat)`` with empty string (`#1635`_)
- Doc fixes (`#1854`_, `#1874`_, `#1884`_)
- Update issue templates (`#1880`_)
- Favor concrete types over "Iterable" (`#1882`_)
- Pass last valid ``CallbackContext`` to ``TIMEOUT`` handlers of ``ConversationHandler`` (`#1826`_)
- Tweak handling of persistence and update persistence after job calls (`#1827`_)
- Use checkout@v2 for GitHub actions (`#1887`_)
.. _`#1858`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1858
.. _`#1635`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1635
.. _`#1854`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1854
.. _`#1874`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1874
.. _`#1884`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1884
.. _`#1880`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1880
.. _`#1882`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1882
.. _`#1826`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1826
.. _`#1827`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1827
.. _`#1887`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1887
Version 12.5.1
==============
*Released 2020-03-30*
**Minor changes, doc fixes or bug fixes:**
- Add missing docs for `PollHandler` and `PollAnswerHandler` (`#1853`_)
- Fix wording in `Filters` docs (`#1855`_)
- Reorder tests to make them more stable (`#1835`_)
- Make `ConversationHandler` attributes immutable (`#1756`_)
- Make `PrefixHandler` attributes `command` and `prefix` editable (`#1636`_)
- Fix UTC as default `tzinfo` for `Job` (`#1696`_)
.. _`#1853`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1853
.. _`#1855`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1855
.. _`#1835`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1835
.. _`#1756`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1756
.. _`#1636`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1636
.. _`#1696`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1696
Version 12.5
============
*Released 2020-03-29*
+1 -1
View File
@@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support
====================
All types and methods of the Telegram Bot API **4.6** are supported.
All types and methods of the Telegram Bot API **4.7** are supported.
==========
Installing
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '12.5' # telegram.__version__[:3]
version = '12.6' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '12.5' # telegram.__version__
release = '12.6' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+6
View File
@@ -0,0 +1,6 @@
telegram.BotCommand
===================
.. autoclass:: telegram.BotCommand
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
telegram.Dice
=============
.. autoclass:: telegram.Dice
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
telegram.ext.PollAnswerHandler
==============================
.. autoclass:: telegram.ext.PollAnswerHandler
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
telegram.ext.PollHandler
========================
.. autoclass:: telegram.ext.PollHandler
:members:
:show-inheritance:
+2
View File
@@ -26,6 +26,8 @@ Handlers
telegram.ext.commandhandler
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.pollanswerhandler
telegram.ext.pollhandler
telegram.ext.precheckoutqueryhandler
telegram.ext.prefixhandler
telegram.ext.regexhandler
+2
View File
@@ -9,6 +9,7 @@ telegram package
telegram.animation
telegram.audio
telegram.bot
telegram.botcommand
telegram.callbackquery
telegram.chat
telegram.chataction
@@ -17,6 +18,7 @@ telegram package
telegram.chatphoto
telegram.constants
telegram.contact
telegram.dice
telegram.document
telegram.error
telegram.file
+4 -1
View File
@@ -19,6 +19,7 @@
"""A library that provides a Python interface to the Telegram Bot API"""
from .base import TelegramObject
from .botcommand import BotCommand
from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
@@ -36,6 +37,7 @@ from .files.location import Location
from .files.venue import Venue
from .files.videonote import VideoNote
from .chataction import ChatAction
from .dice import Dice
from .userprofilephotos import UserProfilePhotos
from .keyboardbutton import KeyboardButton
from .keyboardbuttonpolltype import KeyboardButtonPollType
@@ -157,5 +159,6 @@ __all__ = [
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', 'Dice',
'BotCommand'
]
+395 -177
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Bot Command."""
from telegram import TelegramObject
class BotCommand(TelegramObject):
"""
This object represents a bot command.
Attributes:
command (:obj:`str`): Text of the command.
description (:obj:`str`): Description of the command.
Args:
command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase
English letters, digits and underscores.
description (:obj:`str`): Description of the command, 3-256 characters.
"""
def __init__(self, command, description, **kwargs):
self.command = command
self.description = description
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Dice."""
from telegram import TelegramObject
class Dice(TelegramObject):
"""
This object represents a dice with random value from 1 to 6. (The singular form of "dice" is
"die". However, PTB mimics the Telegram API, which uses the term "dice".)
Attributes:
value (:obj:`int`): Value of the dice.
Args:
value (:obj:`int`): Value of the dice, 1-6.
"""
def __init__(self, value, **kwargs):
self.value = value
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)
+31 -6
View File
@@ -302,6 +302,10 @@ class PrefixHandler(CommandHandler):
pass_user_data=False,
pass_chat_data=False):
self._prefix = list()
self._command = list()
self._commands = list()
super(PrefixHandler, self).__init__(
'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args,
pass_update_queue=pass_update_queue,
@@ -309,15 +313,36 @@ class PrefixHandler(CommandHandler):
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
self.prefix = prefix
self.command = command
self._build_commands()
@property
def prefix(self):
return self._prefix
@prefix.setter
def prefix(self, prefix):
if isinstance(prefix, string_types):
self.prefix = [prefix.lower()]
self._prefix = [prefix.lower()]
else:
self.prefix = prefix
self._prefix = prefix
self._build_commands()
@property
def command(self):
return self._command
@command.setter
def command(self, command):
if isinstance(command, string_types):
self.command = [command.lower()]
self._command = [command.lower()]
else:
self.command = command
self.command = [x.lower() + y.lower() for x in self.prefix for y in self.command]
self._command = command
self._build_commands()
def _build_commands(self):
self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command]
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -334,7 +359,7 @@ class PrefixHandler(CommandHandler):
if message.text:
text_list = message.text.split()
if text_list[0].lower() not in self.command:
if text_list[0].lower() not in self._commands:
return None
filter_result = self.filters(update)
if filter_result:
+102 -17
View File
@@ -29,10 +29,11 @@ from telegram.utils.promise import Promise
class _ConversationTimeoutContext(object):
def __init__(self, conversation_key, update, dispatcher):
def __init__(self, conversation_key, update, dispatcher, callback_context):
self.conversation_key = conversation_key
self.update = update
self.dispatcher = dispatcher
self.callback_context = callback_context
class ConversationHandler(Handler):
@@ -96,8 +97,9 @@ class ConversationHandler(Handler):
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 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`.
last received update and the corresponding ``context`` 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
@@ -130,8 +132,9 @@ class ConversationHandler(Handler):
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 or None (default), there will be no timeout. 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`.
received update and the corresponding ``context`` 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
@@ -165,23 +168,23 @@ class ConversationHandler(Handler):
persistent=False,
map_to_parent=None):
self.entry_points = entry_points
self.states = states
self.fallbacks = fallbacks
self._entry_points = entry_points
self._states = states
self._fallbacks = fallbacks
self.allow_reentry = allow_reentry
self.per_user = per_user
self.per_chat = per_chat
self.per_message = per_message
self.conversation_timeout = conversation_timeout
self.name = name
self._allow_reentry = allow_reentry
self._per_user = per_user
self._per_chat = per_chat
self._per_message = per_message
self._conversation_timeout = conversation_timeout
self._name = name
if persistent and not self.name:
raise ValueError("Conversations can't be persistent when handler is unnamed.")
self.persistent = persistent
self._persistence = None
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
Set by dispatcher"""
self.map_to_parent = map_to_parent
self._map_to_parent = map_to_parent
self.timeout_jobs = dict()
self._timeout_jobs_lock = Lock()
@@ -225,6 +228,87 @@ class ConversationHandler(Handler):
"since inline queries have no chat context.")
break
@property
def entry_points(self):
return self._entry_points
@entry_points.setter
def entry_points(self, value):
raise ValueError('You can not assign a new value to entry_points after initialization.')
@property
def states(self):
return self._states
@states.setter
def states(self, value):
raise ValueError('You can not assign a new value to states after initialization.')
@property
def fallbacks(self):
return self._fallbacks
@fallbacks.setter
def fallbacks(self, value):
raise ValueError('You can not assign a new value to fallbacks after initialization.')
@property
def allow_reentry(self):
return self._allow_reentry
@allow_reentry.setter
def allow_reentry(self, value):
raise ValueError('You can not assign a new value to allow_reentry after initialization.')
@property
def per_user(self):
return self._per_user
@per_user.setter
def per_user(self, value):
raise ValueError('You can not assign a new value to per_user after initialization.')
@property
def per_chat(self):
return self._per_chat
@per_chat.setter
def per_chat(self, value):
raise ValueError('You can not assign a new value to per_chat after initialization.')
@property
def per_message(self):
return self._per_message
@per_message.setter
def per_message(self, value):
raise ValueError('You can not assign a new value to per_message after initialization.')
@property
def conversation_timeout(self):
return self._conversation_timeout
@conversation_timeout.setter
def conversation_timeout(self, value):
raise ValueError('You can not assign a new value to conversation_timeout after '
'initialization.')
@property
def name(self):
return self._name
@name.setter
def name(self, value):
raise ValueError('You can not assign a new value to name after initialization.')
@property
def map_to_parent(self):
return self._map_to_parent
@map_to_parent.setter
def map_to_parent(self, value):
raise ValueError('You can not assign a new value to map_to_parent after initialization.')
@property
def persistence(self):
return self._persistence
@@ -385,7 +469,8 @@ class ConversationHandler(Handler):
# Add the new timeout job
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
context=_ConversationTimeoutContext(conversation_key, update,
dispatcher, context))
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
self.update_state(self.END, conversation_key)
@@ -422,9 +507,9 @@ class ConversationHandler(Handler):
callback_context = None
if isinstance(context, CallbackContext):
job = context.job
callback_context = context
context = job.context
callback_context = context.callback_context
with self._timeout_jobs_lock:
found_job = self.timeout_jobs[context.conversation_key]
+4
View File
@@ -226,6 +226,8 @@ class DictPersistence(BasePersistence):
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id].
"""
if self._user_data is None:
self._user_data = defaultdict(dict)
if self._user_data.get(user_id) == data:
return
self._user_data[user_id] = data
@@ -238,6 +240,8 @@ class DictPersistence(BasePersistence):
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
if self._chat_data is None:
self._chat_data = defaultdict(dict)
if self._chat_data.get(chat_id) == data:
return
self._chat_data[chat_id] = data
+52 -55
View File
@@ -323,53 +323,6 @@ class Dispatcher(object):
"""
def persist_update(update):
"""Persist a single update.
Args:
update (:class:`telegram.Update`):
The update to process.
"""
if self.persistence and isinstance(update, Update):
if self.persistence.store_bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving bot 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_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 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 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):
try:
@@ -388,13 +341,13 @@ class Dispatcher(object):
if not context and self.use_context:
context = CallbackContext.from_update(update, self)
handler.handle_update(update, self, check, context)
persist_update(update)
self.update_persistence(update=update)
break
# Stop processing with any other handler.
except DispatcherHandlerStop:
self.logger.debug('Stopping further handlers due to DispatcherHandlerStop')
persist_update(update)
self.update_persistence(update=update)
break
# Dispatch any error.
@@ -471,18 +424,62 @@ class Dispatcher(object):
del self.handlers[group]
self.groups.remove(group)
def update_persistence(self):
def update_persistence(self, update=None):
"""Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`.
Args:
update (:class:`telegram.Update`, optional): The update to process. If passed, only the
corresponding ``user_data`` and ``chat_data`` will be updated.
"""
if self.persistence:
chat_ids = self.chat_data.keys()
user_ids = self.user_data.keys()
if isinstance(update, Update):
if update.effective_chat:
chat_ids = [update.effective_chat.id]
else:
chat_ids = []
if update.effective_user:
user_ids = [update.effective_user.id]
else:
user_ids = []
if self.persistence.store_bot_data:
self.persistence.update_bot_data(self.bot_data)
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving bot 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_chat_data:
for chat_id in self.chat_data:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
for chat_id in chat_ids:
try:
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:
for user_id in self.user_data:
self.persistence.update_user_data(user_id, self.user_data[user_id])
for user_id in user_ids:
try:
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)
def add_error_handler(self, callback):
"""Registers an error handler in the Dispatcher. This handler will receive every error
+70 -24
View File
@@ -50,7 +50,7 @@ class BaseFilter(object):
>>> Filters.text & (~ Filters.forwarded)
Note:
Filters use the same short circuiting logic that pythons `and`, `or` and `not`.
Filters use the same short circuiting logic as python's `and`, `or` and `not`.
This means that for example:
>>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)')
@@ -236,35 +236,35 @@ class Filters(object):
class _Text(BaseFilter):
name = 'Filters.text'
class _TextIterable(BaseFilter):
class _TextStrings(BaseFilter):
def __init__(self, iterable):
self.iterable = iterable
self.name = 'Filters.text({})'.format(iterable)
def __init__(self, strings):
self.strings = strings
self.name = 'Filters.text({})'.format(strings)
def filter(self, message):
if message.text:
return message.text in self.iterable
return message.text in self.strings
return False
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._TextIterable(update)
return self._TextStrings(update)
def filter(self, message):
return bool(message.text)
text = _Text()
"""Text Messages. If an iterable of strings is passed, it filters messages to only allow those
whose text is appearing in the given iterable.
"""Text Messages. If a list of strings is passed, it filters messages to only allow those
whose text is appearing in the given list.
Examples:
To allow any text message, simply use
``MessageHandler(Filters.text, callback_method)``.
A simple usecase for passing an iterable is to allow only messages that were send by a
A simple usecase for passing a list is to allow only messages that were send by a
custom :class:`telegram.ReplyKeyboardMarkup`::
buttons = ['Start', 'Settings', 'Back']
@@ -272,44 +272,48 @@ class Filters(object):
...
MessageHandler(Filters.text(buttons), callback_method)
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
Args:
update (Iterable[:obj:`str`], optional): Which messages to allow. Only exact matches
are allowed. If not specified, will allow any text message.
update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only
exact matches are allowed. If not specified, will allow any text message.
"""
class _Caption(BaseFilter):
name = 'Filters.caption'
class _CaptionIterable(BaseFilter):
class _CaptionStrings(BaseFilter):
def __init__(self, iterable):
self.iterable = iterable
self.name = 'Filters.caption({})'.format(iterable)
def __init__(self, strings):
self.strings = strings
self.name = 'Filters.caption({})'.format(strings)
def filter(self, message):
if message.caption:
return message.caption in self.iterable
return message.caption in self.strings
return False
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._CaptionIterable(update)
return self._CaptionStrings(update)
def filter(self, message):
return bool(message.caption)
caption = _Caption()
"""Messages with a caption. If an iterable of strings is passed, it filters messages to only
allow those whose caption is appearing in the given iterable.
"""Messages with a caption. If a list of strings is passed, it filters messages to only
allow those whose caption is appearing in the given list.
Examples:
``MessageHandler(Filters.caption, callback_method)``
Args:
update (Iterable[:obj:`str`], optional): Which captions to allow. Only exact matches
are allowed. If not specified, will allow any message with a caption.
update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only
exact matches are allowed. If not specified, will allow any message with a caption.
"""
class _Command(BaseFilter):
@@ -368,7 +372,7 @@ class Filters(object):
if you need to specify flags on your pattern.
Note:
Filters use the same short circuiting logic that pythons `and`, `or` and `not`.
Filters use the same short circuiting logic as python's `and`, `or` and `not`.
This means that for example:
>>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)')
@@ -427,7 +431,7 @@ class Filters(object):
send media with wrong types that don't fit to this handler.
Example:
Filters.documents.category('audio/') returnes `True` for all types
Filters.documents.category('audio/') returns `True` for all types
of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'
"""
@@ -957,6 +961,48 @@ officedocument.wordprocessingml.document")``-
poll = _Poll()
"""Messages that contain a :class:`telegram.Poll`."""
class _Dice(BaseFilter):
name = 'Filters.dice'
class _DiceValues(BaseFilter):
def __init__(self, values):
self.values = [values] if isinstance(values, int) else values
self.name = 'Filters.dice({})'.format(values)
def filter(self, message):
return bool(message.dice and message.dice.value in self.values)
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._DiceValues(update)
def filter(self, message):
return bool(message.dice)
dice = _Dice()
"""Dice Messages. If an integer or a list of integers is passed, it filters messages to only
allow those whose dice value is appearing in the given list.
Examples:
To allow any dice message, simply use
``MessageHandler(Filters.dice, callback_method)``.
To allow only dice with value 6, use
``MessageHandler(Filters.dice(6), callback_method)``.
To allow only dice with value 5 `or` 6, use
``MessageHandler(Filters.dice([5, 6]), callback_method)``.
Args:
update (:obj:`int` | List[:obj:`int`], optional): Which values to allow. If not
specified, will allow any dice message.
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
"""
class language(BaseFilter):
"""Filters messages to only allow those which are from users with a certain language code.
+10 -7
View File
@@ -129,10 +129,11 @@ class JobQueue(object):
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run.
which the job should run. If the timezone (``datetime.tzinfo``) is ``None``, UTC
will be assumed.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow.
tomorrow. If the timezone (``time.tzinfo``) is ``None``, UTC will be assumed.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
@@ -172,10 +173,11 @@ class JobQueue(object):
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run.
which the job should run. If the timezone (``datetime.tzinfo``) is ``None``, UTC
will be assumed.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow.
tomorrow. If the timezone (``time.tzinfo``) is ``None``, UTC will be assumed.
Defaults to ``interval``
context (:obj:`object`, optional): Additional data needed for the callback function.
@@ -285,9 +287,10 @@ class JobQueue(object):
if job.enabled:
try:
current_week_day = datetime.datetime.now(job.tzinfo).date().weekday()
if any(day == current_week_day for day in job.days):
if current_week_day in job.days:
self.logger.debug('Running job %s', job.name)
job.run(self._dispatcher)
self._dispatcher.update_persistence()
except Exception:
self.logger.exception('An uncaught error was raised while executing job %s',
@@ -400,7 +403,7 @@ class Job(object):
days=Days.EVERY_DAY,
name=None,
job_queue=None,
tzinfo=_UTC):
tzinfo=None):
self.callback = callback
self.context = context
@@ -413,7 +416,7 @@ class Job(object):
self._days = None
self.days = days
self.tzinfo = tzinfo
self.tzinfo = tzinfo or _UTC
self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None
+4
View File
@@ -224,6 +224,8 @@ class PicklePersistence(BasePersistence):
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id].
"""
if self.user_data is None:
self.user_data = defaultdict(dict)
if self.user_data.get(user_id) == data:
return
self.user_data[user_id] = data
@@ -242,6 +244,8 @@ class PicklePersistence(BasePersistence):
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
if self.chat_data is None:
self.chat_data = defaultdict(dict)
if self.chat_data.get(chat_id) == data:
return
self.chat_data[chat_id] = data
+3 -3
View File
@@ -570,9 +570,9 @@ class Updater(object):
"""Blocks until one of the signals are received and stops the updater.
Args:
stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that
should be subscribed to. Updater.stop() will be called on receiving one of those
signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal
module that should be subscribed to. Updater.stop() will be called on receiving one
of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
"""
for sig in stop_signals:
+9 -1
View File
@@ -138,6 +138,8 @@ class StickerSet(TelegramObject):
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.
thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the .WEBP or .TGS
format
Args:
name (:obj:`str`): Sticker set name.
@@ -145,15 +147,20 @@ class StickerSet(TelegramObject):
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.
thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the .WEBP or .TGS
format
"""
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs):
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, thumb=None,
**kwargs):
self.name = name
self.title = title
self.is_animated = is_animated
self.contains_masks = contains_masks
self.stickers = stickers
# Optionals
self.thumb = thumb
self._id_attrs = (self.name,)
@@ -164,6 +171,7 @@ class StickerSet(TelegramObject):
data = super(StickerSet, StickerSet).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['stickers'] = Sticker.de_list(data.get('stickers'), bot)
return StickerSet(bot=bot, **data)
+8 -7
View File
@@ -30,27 +30,28 @@ class InlineKeyboardButton(TelegramObject):
Attributes:
text (:obj:`str`): Label text on the button.
url (:obj:`str`): Optional. HTTP url to be opened when button is pressed.
url (:obj:`str`): Optional. HTTP or tg:// url to be opened when button is pressed.
login_url (:class:`telegram.LoginUrl`) Optional. An HTTP URL used to automatically
authorize the user.
authorize the user. Can be used as a replacement for the Telegram Login Widget.
callback_data (:obj:`str`): Optional. Data to be sent in a callback query to the bot when
button is pressed, UTF-8 1-64 bytes.
switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their
chats, open that chat and insert the bot's username and the specified inline query in
the input field.
the input field. Can be empty, in which case just the bots username will be inserted.
switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and
the specified inline query in the current chat's input field.
the specified inline query in the current chat's input field. Can be empty, in which
case just the bots username will be inserted.
callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
be launched when the user presses the button.
pay (:obj:`bool`): Optional. Specify True, to send a Pay button.
Args:
text (:obj:`str`): Label text on the button.
url (:obj:`str`): HTTP url to be opened when button is pressed.
url (:obj:`str`): HTTP or tg:// url to be opened when button is pressed.
login_url (:class:`telegram.LoginUrl`, optional) An HTTP URL used to automatically
authorize the user.
authorize the user. Can be used as a replacement for the Telegram Login Widget.
callback_data (:obj:`str`, optional): Data to be sent in a callback query to the bot when
button is pressed, 1-64 UTF-8 bytes.
button is pressed, UTF-8 1-64 bytes.
switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the
user to select one of their chats, open that chat and insert the bot's username and the
specified inline query in the input field. Can be empty, in which case just the bot's
+6 -6
View File
@@ -33,9 +33,9 @@ class InlineQueryResultAudio(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_url (:obj:`str`): A valid URL for the audio file.
title (:obj:`str`): Title.
performer (:obj:`str`): Optional. Caption, 0-200 characters.
audio_duration (:obj:`str`): Optional. Performer.
caption (:obj:`str`): Optional. Audio duration in seconds.
performer (:obj:`str`): Optional. Performer.
audio_duration (:obj:`str`): Optional. Audio duration in seconds.
caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -48,9 +48,9 @@ class InlineQueryResultAudio(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_url (:obj:`str`): A valid URL for the audio file.
title (:obj:`str`): Title.
performer (:obj:`str`, optional): Caption, 0-200 characters.
audio_duration (:obj:`str`, optional): Performer.
caption (:obj:`str`, optional): Audio duration in seconds.
performer (:obj:`str`, optional): Performer.
audio_duration (:obj:`str`, optional): Audio duration in seconds.
caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -26,13 +26,13 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
"""
Represents a link to an mp3 audio file stored on the Telegram servers. By default, this audio
file will be sent by the user. Alternatively, you can use :attr:`input_message_content` to
send amessage with the specified content instead of the audio.
send a message with the specified content instead of the audio.
Attributes:
type (:obj:`str`): 'audio'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -44,7 +44,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -34,7 +34,8 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -48,7 +49,8 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -34,7 +34,8 @@ class InlineQueryResultCachedGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -47,7 +48,8 @@ class InlineQueryResultCachedGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -34,7 +34,8 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -47,7 +48,8 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
title (:obj:`str`, optional): Title for the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -35,7 +35,8 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
photo_file_id (:obj:`str`): A valid file identifier of the photo.
title (:obj:`str`): Optional. Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -49,7 +50,8 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
photo_file_id (:obj:`str`): A valid file identifier of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -37,8 +37,8 @@ class InlineQueryResultCachedSticker(InlineQueryResult):
message to be sent instead of the sticker.
Args:
id (:obj:`str`):
sticker_file_id (:obj:`str`):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
sticker_file_id (:obj:`str`): A valid file identifier of the sticker.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -35,7 +35,8 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -49,7 +50,8 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+6 -4
View File
@@ -33,7 +33,8 @@ class InlineQueryResultDocument(InlineQueryResult):
type (:obj:`str`): 'document'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -52,7 +53,8 @@ class InlineQueryResultDocument(InlineQueryResult):
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -60,9 +62,9 @@ class InlineQueryResultDocument(InlineQueryResult):
mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf"
or "application/zip".
description (:obj:`str`, optional): Short description of the result.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the file.
thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file.
thumb_width (:obj:`int`, optional): Thumbnail width.
+7 -5
View File
@@ -37,14 +37,15 @@ class InlineQueryResultGif(InlineQueryResult):
gif_duration (:obj:`int`): Optional. Duration of the GIF.
thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the gif.
message to be sent instead of the GIF animation.
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -53,15 +54,16 @@ class InlineQueryResultGif(InlineQueryResult):
gif_height (:obj:`int`, optional): Height of the GIF.
gif_duration (:obj:`int`, optional): Duration of the GIF
thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
caption (:obj:`str`, optional): Caption, 0-1024 characters
title (:obj:`str`, optional): Title for the result.
caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the gif.
message to be sent instead of the GIF animation.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
+6 -4
View File
@@ -38,14 +38,15 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_duration (:obj:`int`): Optional. Video duration.
thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the MPEG-4 file.
message to be sent instead of the video animation.
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -55,14 +56,15 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_duration (:obj:`int`, optional): Video duration.
thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
title (:obj:`str`, optional): Title for the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the MPEG-4 file.
message to be sent instead of the video animation.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
+4 -2
View File
@@ -38,7 +38,8 @@ class InlineQueryResultPhoto(InlineQueryResult):
photo_height (:obj:`int`): Optional. Height of the photo.
title (:obj:`str`): Optional. Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -56,7 +57,8 @@ class InlineQueryResultPhoto(InlineQueryResult):
photo_height (:obj:`int`, optional): Height of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-1024 characters
caption (:obj:`str`, optional): Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+12 -3
View File
@@ -29,6 +29,10 @@ class InlineQueryResultVideo(InlineQueryResult):
:attr:`input_message_content` to send a message with the specified content instead of
the video.
Note:
If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must
replace its content using :attr:`input_message_content`.
Attributes:
type (:obj:`str`): 'video'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -36,7 +40,8 @@ class InlineQueryResultVideo(InlineQueryResult):
mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
title (:obj:`str`): Title for the result.
caption (:obj:`str`): Optional. Caption, 0-1024 characters
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -47,7 +52,9 @@ class InlineQueryResultVideo(InlineQueryResult):
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the video.
message to be sent instead of the video. This field is required if
InlineQueryResultVideo is used to send an HTML-page as a result
(e.g., a YouTube video).
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
@@ -66,7 +73,9 @@ class InlineQueryResultVideo(InlineQueryResult):
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the video.
message to be sent instead of the video. This field is required if
InlineQueryResultVideo is used to send an HTML-page as a result
(e.g., a YouTube video).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
+6 -6
View File
@@ -33,30 +33,30 @@ class InlineQueryResultVoice(InlineQueryResult):
type (:obj:`str`): 'voice'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_url (:obj:`str`): A valid URL for the voice recording.
title (:obj:`str`): Voice message title.
title (:obj:`str`): Recording title.
caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
voice_duration (:obj:`int`): Optional. Recording duration in seconds.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
message to be sent instead of the voice.
message to be sent instead of the voice recording.
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_url (:obj:`str`): A valid URL for the voice recording.
title (:obj:`str`): Voice message title.
title (:obj:`str`): Recording title.
caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
voice_duration (:obj:`int`, optional): Recording duration in seconds.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
message to be sent instead of the voice.
message to be sent instead of the voice recording.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
+25 -3
View File
@@ -23,7 +23,7 @@ from html import escape
from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker,
TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice,
SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup)
SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup, Dice)
from telegram import ParseMode
from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp
@@ -106,6 +106,7 @@ class Message(TelegramObject):
passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data.
poll (:class:`telegram.Poll`): Optional. Message is a native poll,
information about the poll.
dice (:class:`telegram.Dice`): Optional. Message is a dice.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
@@ -199,7 +200,7 @@ class Message(TelegramObject):
smaller than 52 bits, so a signed 64 bit integer or double-precision float type are
safe for storing this identifier.
pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note
that the Message object in this field will not contain further attr:`reply_to_message`
that the Message object in this field will not contain further :attr:`reply_to_message`
fields even if it is itself a reply.
invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment,
information about the invoice.
@@ -214,6 +215,7 @@ class Message(TelegramObject):
passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data.
poll (:class:`telegram.Poll`, optional): Message is a native poll,
information about the poll.
dice (:class:`telegram.Dice`, optional): Message is a dice with random value from 1 to 6.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message. login_url buttons are represented as ordinary url buttons.
default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the
@@ -229,7 +231,7 @@ class Message(TelegramObject):
MESSAGE_TYPES = ['text', 'new_chat_members', 'left_chat_member', 'new_chat_title',
'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id',
'migrate_from_chat_id', 'pinned_message',
'migrate_from_chat_id', 'pinned_message', 'poll', 'dice',
'passport_data'] + ATTACHMENT_TYPES
def __init__(self,
@@ -282,6 +284,7 @@ class Message(TelegramObject):
reply_markup=None,
bot=None,
default_quote=None,
dice=None,
**kwargs):
# Required
self.message_id = int(message_id)
@@ -331,6 +334,7 @@ class Message(TelegramObject):
self.animation = animation
self.passport_data = passport_data
self.poll = poll
self.dice = dice
self.reply_markup = reply_markup
self.bot = bot
self.default_quote = default_quote
@@ -404,6 +408,7 @@ class Message(TelegramObject):
data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot)
data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot)
data['poll'] = Poll.de_json(data.get('poll'), bot)
data['dice'] = Dice.de_json(data.get('dice'), bot)
data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot)
return cls(bot=bot, **data)
@@ -808,6 +813,23 @@ class Message(TelegramObject):
self._quote(kwargs)
return self.bot.send_poll(self.chat_id, *args, **kwargs)
def reply_dice(self, *args, **kwargs):
"""Shortcut for::
bot.send_dice(update.message.chat_id, *args, **kwargs)
Keyword Args:
quote (:obj:`bool`, optional): If set to ``True``, the dice is sent as an actual reply
to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
self._quote(kwargs)
return self.bot.send_dice(self.chat_id, *args, **kwargs)
def forward(self, chat_id, *args, **kwargs):
"""Shortcut for::
+5 -3
View File
@@ -32,10 +32,12 @@ class PersonalDetails(TelegramObject):
country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code).
residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country
code).
first_name (:obj:`str`): First Name in the language of the user's country of residence.
middle_name (:obj:`str`): Optional. Middle Name in the language of the user's country of
first_name_native (:obj:`str`): First Name in the language of the user's country of
residence.
middle_name_native (:obj:`str`): Optional. Middle Name in the language of the user's
country of residence.
last_name_native (:obj:`str`): Last Name in the language of the user's country of
residence.
last_name (:obj:`str`): Last Name in the language of the user's country of residence.
"""
def __init__(self, first_name, last_name, birth_date, gender, country_code,
+1 -1
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.5'
__version__ = '12.6'
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
+72 -16
View File
@@ -26,7 +26,7 @@ from future.utils import string_types
from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup,
InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent,
ShippingOption, LabeledPrice, ChatPermissions, Poll,
ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand,
InlineQueryResultDocument)
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
from telegram.utils.helpers import from_timestamp, escape_markdown
@@ -80,6 +80,7 @@ class TestBot(object):
@pytest.mark.timeout(10)
def test_get_me_and_properties(self, bot):
get_me_bot = bot.get_me()
commands = bot.get_my_commands()
assert isinstance(get_me_bot, User)
assert get_me_bot.id == bot.id
@@ -91,6 +92,7 @@ class TestBot(object):
assert get_me_bot.can_read_all_group_messages == bot.can_read_all_group_messages
assert get_me_bot.supports_inline_queries == bot.supports_inline_queries
assert 'https://t.me/{}'.format(get_me_bot.username) == bot.link
assert commands == bot.commands
@flaky(3, 1)
@pytest.mark.timeout(10)
@@ -174,7 +176,13 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_and_stop_poll(self, bot, super_group_id):
@pytest.mark.parametrize('reply_markup', [
None,
InlineKeyboardMarkup.from_button(InlineKeyboardButton(text='text', callback_data='data')),
InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='text', callback_data='data')).to_dict()
])
def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
question = 'Is this a test?'
answers = ['Yes', 'No', 'Maybe']
message = bot.send_poll(chat_id=super_group_id, question=question, options=answers,
@@ -190,7 +198,10 @@ class TestBot(object):
assert not message.poll.is_closed
assert message.poll.type == Poll.REGULAR
poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, timeout=60)
# Since only the poll and not the complete message is returned, we can't check that the
# reply_markup is correct. So we just test that sending doesn't give an error.
poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id,
reply_markup=reply_markup, timeout=60)
assert isinstance(poll, Poll)
assert poll.is_closed
assert poll.options[0].text == answers[0]
@@ -210,15 +221,10 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_game(self, bot, chat_id):
game_short_name = 'test_game'
message = bot.send_game(chat_id, game_short_name)
def test_send_dice(self, bot, chat_id):
message = bot.send_dice(chat_id)
assert message.game
assert message.game.description == ('A no-op test game, for python-telegram-bot '
'bot framework testing.')
assert message.game.animation.file_id != ''
assert message.game.photo[0].file_size == 851
assert message.dice
@flaky(3, 1)
@pytest.mark.timeout(10)
@@ -596,6 +602,18 @@ class TestBot(object):
def test_delete_chat_sticker_set(self):
pass
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_game(self, bot, chat_id):
game_short_name = 'test_game'
message = bot.send_game(chat_id, game_short_name)
assert message.game
assert message.game.description == ('A no-op test game, for python-telegram-bot '
'bot framework testing.')
assert message.game.animation.file_id != ''
assert message.game.photo[0].file_size == 851
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_1(self, bot, chat_id):
@@ -795,17 +813,17 @@ class TestBot(object):
assert isinstance(invite_link, string_types)
assert invite_link != ''
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_delete_chat_photo(self, bot, channel_id):
assert bot.delete_chat_photo(channel_id)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_chat_photo(self, bot, channel_id):
with open('tests/data/telegram_test_channel.jpg', 'rb') as f:
assert bot.set_chat_photo(channel_id, f)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_delete_chat_photo(self, bot, channel_id):
assert bot.delete_chat_photo(channel_id)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_chat_title(self, bot, channel_id):
@@ -904,3 +922,41 @@ class TestBot(object):
def test_send_message_default_quote(self, default_bot, chat_id):
message = default_bot.send_message(chat_id, 'test')
assert message.default_quote is True
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_and_get_my_commands(self, bot):
commands = [
BotCommand('cmd1', 'descr1'),
BotCommand('cmd2', 'descr2'),
]
bot.set_my_commands([])
assert bot.get_my_commands() == []
assert bot.commands == []
assert bot.set_my_commands(commands)
for bc in [bot.get_my_commands(), bot.commands]:
assert len(bc) == 2
assert bc[0].command == 'cmd1'
assert bc[0].description == 'descr1'
assert bc[1].command == 'cmd2'
assert bc[1].description == 'descr2'
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_and_get_my_commands_strings(self, bot):
commands = [
['cmd1', 'descr1'],
['cmd2', 'descr2'],
]
bot.set_my_commands([])
assert bot.get_my_commands() == []
assert bot.commands == []
assert bot.set_my_commands(commands)
for bc in [bot.get_my_commands(), bot.commands]:
assert len(bc) == 2
assert bc[0].command == 'cmd1'
assert bc[0].description == 'descr1'
assert bc[1].command == 'cmd2'
assert bc[1].description == 'descr2'
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import BotCommand
@pytest.fixture(scope="class")
def bot_command():
return BotCommand(command='start', description='A command')
class TestBotCommand(object):
command = 'start'
description = 'A command'
def test_de_json(self, bot):
json_dict = {'command': self.command, 'description': self.description}
bot_command = BotCommand.de_json(json_dict, bot)
assert bot_command.command == self.command
assert bot_command.description == self.description
assert BotCommand.de_json(None, bot) is None
def test_to_dict(self, bot_command):
bot_command_dict = bot_command.to_dict()
assert isinstance(bot_command_dict, dict)
assert bot_command_dict['command'] == bot_command.command
assert bot_command_dict['description'] == bot_command.description
+23
View File
@@ -379,6 +379,29 @@ class TestPrefixHandler(BaseTest):
assert not is_match(handler, make_message_update('/test'))
assert not mock_filter.tested
def test_edit_prefix(self):
handler = self.make_default_handler()
handler.prefix = ['?', '§']
assert handler._commands == list(combinations(['?', '§'], self.COMMANDS))
handler.prefix = '+'
assert handler._commands == list(combinations(['+'], self.COMMANDS))
def test_edit_command(self):
handler = self.make_default_handler()
handler.command = 'foo'
assert handler._commands == list(combinations(self.PREFIXES, ['foo']))
def test_basic_after_editing(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
assert self.response(dp, make_message_update(text))
handler.command = 'foo'
text = prefix + 'foo'
assert self.response(dp, make_message_update(text))
def test_context(self, cdp, prefix_message_update):
handler = self.make_default_handler(self.callback_context)
cdp.add_handler(handler)
+69
View File
@@ -179,6 +179,38 @@ class TestConversationHandler(object):
return self._set_state(update, self.STOPPING)
# Tests
@pytest.mark.parametrize('attr', ['entry_points', 'states', 'fallbacks', 'per_chat', 'name',
'per_user', 'allow_reentry', 'conversation_timeout', 'map_to_parent'],
indirect=False)
def test_immutable(self, attr):
ch = ConversationHandler('entry_points', {'states': ['states']}, 'fallbacks',
per_chat='per_chat',
per_user='per_user', per_message=False,
allow_reentry='allow_reentry',
conversation_timeout='conversation_timeout',
name='name', map_to_parent='map_to_parent')
value = getattr(ch, attr)
if isinstance(value, list):
assert value[0] == attr
elif isinstance(value, dict):
assert list(value.keys())[0] == attr
else:
assert getattr(ch, attr) == attr
with pytest.raises(ValueError, match='You can not assign a new value to {}'.format(attr)):
setattr(ch, attr, True)
def test_immutable_per_message(self):
ch = ConversationHandler('entry_points', {'states': ['states']}, 'fallbacks',
per_chat='per_chat',
per_user='per_user', per_message=False,
allow_reentry='allow_reentry',
conversation_timeout='conversation_timeout',
name='name', map_to_parent='map_to_parent')
assert ch.per_message is False
with pytest.raises(ValueError, match='You can not assign a new value to per_message'):
ch.per_message = True
def test_per_all_false(self):
with pytest.raises(ValueError, match="can't all be 'False'"):
ConversationHandler(self.entry_points, self.states, self.fallbacks,
@@ -514,6 +546,43 @@ class TestConversationHandler(object):
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
def test_conversation_handler_timeout_update_and_context(self, cdp, bot, user1):
context = None
def start_callback(u, c):
nonlocal context, self
context = c
return self.start(u, c)
states = self.states
timeout_handler = CommandHandler('start', None)
states.update({ConversationHandler.TIMEOUT: [timeout_handler]})
handler = ConversationHandler(entry_points=[CommandHandler('start', start_callback)],
states=states, fallbacks=self.fallbacks,
conversation_timeout=0.5)
cdp.add_handler(handler)
# Start state machine, then reach timeout
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
length=len('/start'))],
bot=bot)
update = Update(update_id=0, message=message)
def timeout_callback(u, c):
nonlocal update, context, self
self.is_timeout = True
assert u is update
assert c is context
timeout_handler.callback = timeout_callback
cdp.process_update(update)
sleep(0.5)
cdp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
def test_conversation_timeout_keeps_extending(self, dp, bot, user1):
handler = ConversationHandler(entry_points=self.entry_points, states=self.states,
fallbacks=self.fallbacks, conversation_timeout=0.5)
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import Dice
@pytest.fixture(scope="class")
def dice():
return Dice(value=5)
class TestDice(object):
value = 4
def test_de_json(self, bot):
json_dict = {'value': self.value}
dice = Dice.de_json(json_dict, bot)
assert dice.value == self.value
assert Dice.de_json(None, bot) is None
def test_to_dict(self, dice):
dice_dict = dice.to_dict()
assert isinstance(dice_dict, dict)
assert dice_dict['value'] == dice.value
+85
View File
@@ -449,3 +449,88 @@ class TestDispatcher(object):
with pytest.warns(TelegramDeprecationWarning):
Dispatcher(dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0,
use_context=False)
def test_error_while_persisting(self, cdp, monkeypatch):
class OwnPersistence(BasePersistence):
def __init__(self):
super(OwnPersistence, self).__init__()
self.store_user_data = True
self.store_chat_data = True
self.store_bot_data = True
def update(self, data):
raise Exception('PersistenceError')
def update_bot_data(self, data):
self.update(data)
def update_chat_data(self, chat_id, data):
self.update(data)
def update_user_data(self, user_id, data):
self.update(data)
def callback(update, context):
pass
test_flag = False
def error(update, context):
nonlocal test_flag
test_flag = str(context.error) == 'PersistenceError'
raise Exception('ErrorHandlingError')
def logger(message):
assert 'uncaught error was raised while handling' in message
update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Text'))
handler = MessageHandler(Filters.all, callback)
cdp.add_handler(handler)
cdp.add_error_handler(error)
monkeypatch.setattr(cdp.logger, 'exception', logger)
cdp.persistence = OwnPersistence()
cdp.process_update(update)
assert test_flag
def test_persisting_no_user_no_chat(self, cdp):
class OwnPersistence(BasePersistence):
def __init__(self):
super(OwnPersistence, self).__init__()
self.store_user_data = True
self.store_chat_data = True
self.store_bot_data = True
self.test_flag_bot_data = False
self.test_flag_chat_data = False
self.test_flag_user_data = False
def update_bot_data(self, data):
self.test_flag_bot_data = True
def update_chat_data(self, chat_id, data):
self.test_flag_chat_data = True
def update_user_data(self, user_id, data):
self.test_flag_user_data = True
def callback(update, context):
pass
handler = MessageHandler(Filters.all, callback)
cdp.add_handler(handler)
cdp.persistence = OwnPersistence()
update = Update(1, message=Message(1, User(1, '', False), None, None, text='Text'))
cdp.process_update(update)
assert cdp.persistence.test_flag_bot_data
assert cdp.persistence.test_flag_user_data
assert not cdp.persistence.test_flag_chat_data
cdp.persistence.test_flag_bot_data = False
cdp.persistence.test_flag_user_data = False
cdp.persistence.test_flag_chat_data = False
update = Update(1, message=Message(1, None, None, Chat(1, ''), text='Text'))
cdp.process_update(update)
assert cdp.persistence.test_flag_bot_data
assert not cdp.persistence.test_flag_user_data
assert cdp.persistence.test_flag_chat_data
+19 -3
View File
@@ -20,7 +20,7 @@ import datetime
import pytest
from telegram import Message, User, Chat, MessageEntity, Document, Update
from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice
from telegram.ext import Filters, BaseFilter
import re
@@ -47,7 +47,7 @@ class TestFilters(object):
update.message.text = '/test'
assert (Filters.text)(update)
def test_filters_text_iterable(self, update):
def test_filters_text_strings(self, update):
update.message.text = '/test'
assert Filters.text({'/test', 'test1'})(update)
assert not Filters.text(['test1', 'test2'])(update)
@@ -58,7 +58,7 @@ class TestFilters(object):
update.message.caption = None
assert not (Filters.caption)(update)
def test_filters_caption_iterable(self, update):
def test_filters_caption_strings(self, update):
update.message.caption = 'test'
assert Filters.caption({'test', 'test1'})(update)
assert not Filters.caption(['test1', 'test2'])(update)
@@ -622,6 +622,22 @@ class TestFilters(object):
update.message.poll = 'test'
assert Filters.poll(update)
def test_filters_dice(self, update):
update.message.dice = Dice(4)
assert Filters.dice(update)
update.message.dice = None
assert not Filters.dice(update)
def test_filters_dice_iterable(self, update):
update.message.dice = None
assert not Filters.dice(5)(update)
update.message.dice = Dice(5)
assert Filters.dice(5)(update)
assert Filters.dice({5, 6})(update)
assert not Filters.dice(1)(update)
assert not Filters.dice([2, 3])(update)
def test_language_filter_single(self, update):
update.message.from_user.language_code = 'en_US'
assert (Filters.language('en_US'))(update)
+21 -1
View File
@@ -20,7 +20,7 @@
import pytest
from flaky import flaky
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyMarkup
@pytest.fixture(scope='class')
@@ -68,6 +68,26 @@ class TestInlineKeyboardMarkup(object):
def test_expected_values(self, inline_keyboard_markup):
assert inline_keyboard_markup.inline_keyboard == self.inline_keyboard
def test_expected_values_empty_switch(self, inline_keyboard_markup, bot, monkeypatch):
def test(url, data, reply_to_message_id=None, disable_notification=None,
reply_markup=None, timeout=None, **kwargs):
if reply_markup is not None:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_json()
else:
data['reply_markup'] = reply_markup
assert bool('"switch_inline_query": ""' in data['reply_markup'])
assert bool('"switch_inline_query_current_chat": ""' in data['reply_markup'])
inline_keyboard_markup.inline_keyboard[0][0].callback_data = None
inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = ''
inline_keyboard_markup.inline_keyboard[0][1].callback_data = None
inline_keyboard_markup.inline_keyboard[0][1].switch_inline_query_current_chat = ''
monkeypatch.setattr(bot, '_message', test)
bot.send_message(123, 'test', reply_markup=inline_keyboard_markup)
def test_to_dict(self, inline_keyboard_markup):
inline_keyboard_markup_dict = inline_keyboard_markup.to_dict()
+12 -1
View File
@@ -28,7 +28,7 @@ from flaky import flaky
from telegram.ext import JobQueue, Updater, Job, CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import _UtcOffsetTimezone
from telegram.utils.helpers import _UtcOffsetTimezone, _UTC
@pytest.fixture(scope='function')
@@ -330,3 +330,14 @@ class TestJobQueue(object):
sleep(0.03)
assert self.result == 0
def test_job_default_tzinfo(self, job_queue):
"""Test that default tzinfo is always set to UTC"""
job_1 = job_queue.run_once(self.job_run_once, 0.01)
job_2 = job_queue.run_repeating(self.job_run_once, 10)
job_3 = job_queue.run_daily(self.job_run_once, time=dtm.time(hour=15))
jobs = [job_1, job_2, job_3]
for job in jobs:
assert job.tzinfo == _UTC
+21 -6
View File
@@ -22,7 +22,7 @@ import pytest
from telegram import (Update, Message, User, MessageEntity, Chat, Audio, Document, Animation,
Game, PhotoSize, Sticker, Video, Voice, VideoNote, Contact, Location, Venue,
Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption)
Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption, Dice)
from tests.test_passport import RAW_PASSPORT_DATA
@@ -97,7 +97,8 @@ def message(bot):
'text': 'start', 'url': 'http://google.com'}, {
'text': 'next', 'callback_data': 'abcd'}],
[{'text': 'Cancel', 'callback_data': 'Cancel'}]]}},
{'quote': True}
{'quote': True},
{'dice': Dice(4)}
],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'caption_entities', 'audio', 'document', 'animation', 'game', 'photo',
@@ -107,7 +108,7 @@ def message(bot):
'migrated_from', 'pinned', 'invoice', 'successful_payment',
'connected_website', 'forward_signature', 'author_signature',
'photo_from_media_group', 'passport_data', 'poll', 'reply_markup',
'default_quote'])
'default_quote', 'dice'])
def message_params(bot, request):
return Message(message_id=TestMessage.id_,
from_user=TestMessage.from_user,
@@ -702,7 +703,7 @@ class TestMessage(object):
def test_reply_poll(self, monkeypatch, message):
def test(*args, **kwargs):
id_ = args[0] == message.chat_id
contact = kwargs['contact'] == 'test_poll'
contact = kwargs['question'] == 'test_poll'
if kwargs.get('reply_to_message_id'):
reply = kwargs['reply_to_message_id'] == message.message_id
else:
@@ -710,8 +711,22 @@ class TestMessage(object):
return id_ and contact and reply
monkeypatch.setattr(message.bot, 'send_poll', test)
assert message.reply_poll(contact='test_poll')
assert message.reply_poll(contact='test_poll', quote=True)
assert message.reply_poll(question='test_poll')
assert message.reply_poll(question='test_poll', quote=True)
def test_reply_dice(self, monkeypatch, message):
def test(*args, **kwargs):
id_ = args[0] == message.chat_id
contact = kwargs['disable_notification'] is True
if kwargs.get('reply_to_message_id'):
reply = kwargs['reply_to_message_id'] == message.message_id
else:
reply = True
return id_ and contact and reply
monkeypatch.setattr(message.bot, 'send_dice', test)
assert message.reply_dice(disable_notification=True)
assert message.reply_dice(disable_notification=True, quote=True)
def test_forward(self, monkeypatch, message):
def test(*args, **kwargs):
+46 -1
View File
@@ -29,12 +29,13 @@ import logging
import os
import pickle
from collections import defaultdict
from time import sleep
import pytest
from telegram import Update, Message, User, Chat, MessageEntity
from telegram.ext import BasePersistence, Updater, ConversationHandler, MessageHandler, Filters, \
PicklePersistence, CommandHandler, DictPersistence, TypeHandler
PicklePersistence, CommandHandler, DictPersistence, TypeHandler, JobQueue
@pytest.fixture(autouse=True)
@@ -87,6 +88,13 @@ def updater(bot, base_persistence):
return u
@pytest.fixture(scope='function')
def job_queue(bot):
jq = JobQueue()
yield jq
jq.stop()
class TestBasePersistence(object):
def test_creation(self, base_persistence):
@@ -920,6 +928,24 @@ class TestPickelPersistence(object):
assert nested_ch.conversations[nested_ch._get_key(update)] == 1
assert nested_ch.conversations == pickle_persistence.conversations['name3']
def test_with_job(self, job_queue, cdp, pickle_persistence):
def job_callback(context):
context.bot_data['test1'] = '456'
context.dispatcher.chat_data[123]['test2'] = '789'
context.dispatcher.user_data[789]['test3'] = '123'
cdp.persistence = pickle_persistence
job_queue.set_dispatcher(cdp)
job_queue.start()
job_queue.run_once(job_callback, 0.01)
sleep(0.05)
bot_data = pickle_persistence.get_bot_data()
assert bot_data == {'test1': '456'}
chat_data = pickle_persistence.get_chat_data()
assert chat_data[123] == {'test2': '789'}
user_data = pickle_persistence.get_user_data()
assert user_data[789] == {'test3': '123'}
@pytest.fixture(scope='function')
def user_data_json(user_data):
@@ -1202,3 +1228,22 @@ class TestDictPersistence(object):
assert ch.conversations == dict_persistence.conversations['name2']
assert nested_ch.conversations[nested_ch._get_key(update)] == 1
assert nested_ch.conversations == dict_persistence.conversations['name3']
def test_with_job(self, job_queue, cdp):
def job_callback(context):
context.bot_data['test1'] = '456'
context.dispatcher.chat_data[123]['test2'] = '789'
context.dispatcher.user_data[789]['test3'] = '123'
dict_persistence = DictPersistence()
cdp.persistence = dict_persistence
job_queue.set_dispatcher(cdp)
job_queue.start()
job_queue.run_once(job_callback, 0.01)
sleep(0.05)
bot_data = dict_persistence.get_bot_data()
assert bot_data == {'test1': '456'}
chat_data = dict_persistence.get_chat_data()
assert chat_data[123] == {'test2': '789'}
user_data = dict_persistence.get_user_data()
assert user_data[789] == {'test3': '123'}
+82 -9
View File
@@ -40,6 +40,19 @@ def sticker(bot, chat_id):
return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker
@pytest.fixture(scope='function')
def animated_sticker_file():
f = open('tests/data/telegram_animated_sticker.tgs', 'rb')
yield f
f.close()
@pytest.fixture(scope='class')
def animated_sticker(bot, chat_id):
with open('tests/data/telegram_animated_sticker.tgs', 'rb') as f:
return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker
class TestSticker(object):
# sticker_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.webp'
# Serving sticker from gh since our server sends wrong content_type
@@ -245,12 +258,27 @@ class TestSticker(object):
@pytest.fixture(scope='function')
def sticker_set(bot):
ss = bot.get_sticker_set('test_by_{0}'.format(bot.username))
ss = bot.get_sticker_set('test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
return ss
@pytest.fixture(scope='function')
def animated_sticker_set(bot):
ss = bot.get_sticker_set('animated_test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
return ss
@pytest.fixture(scope='function')
def sticker_set_thumb_file():
f = open('tests/data/sticker_set_thumb.png', 'rb')
yield f
f.close()
class TestStickerSet(object):
title = 'Test stickers'
is_animated = True
@@ -258,14 +286,15 @@ class TestStickerSet(object):
stickers = [Sticker('file_id', 'file_un_id', 512, 512, True)]
name = 'NOTAREALNAME'
def test_de_json(self, bot):
name = 'test_by_{0}'.format(bot.username)
def test_de_json(self, bot, sticker):
name = 'test_by_{}'.format(bot.username)
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]
'stickers': [x.to_dict() for x in self.stickers],
'thumb': sticker.thumb.to_dict()
}
sticker_set = StickerSet.de_json(json_dict, bot)
@@ -274,15 +303,28 @@ class TestStickerSet(object):
assert sticker_set.is_animated == self.is_animated
assert sticker_set.contains_masks == self.contains_masks
assert sticker_set.stickers == self.stickers
assert sticker_set.thumb == sticker.thumb
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_1(self, bot, chat_id):
def test_bot_methods_1_png(self, bot, chat_id, sticker_file):
with open('tests/data/telegram_sticker.png', 'rb') as f:
file = bot.upload_sticker_file(95205500, f)
assert file
assert bot.add_sticker_to_set(chat_id, 'test_by_{0}'.format(bot.username),
file.file_id, '😄')
assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username),
png_sticker=file.file_id, emojis='😄')
# Also test with file input and mask
assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username),
png_sticker=sticker_file, emojis='😄',
mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2))
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_1_tgs(self, bot, chat_id):
assert bot.add_sticker_to_set(
chat_id, 'animated_test_by_{}'.format(bot.username),
tgs_sticker=open('tests/data/telegram_animated_sticker.tgs', 'rb'),
emojis='😄')
def test_sticker_set_to_dict(self, sticker_set):
sticker_set_dict = sticker_set.to_dict()
@@ -296,17 +338,48 @@ class TestStickerSet(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_2(self, bot, sticker_set):
def test_bot_methods_2_png(self, bot, sticker_set):
file_id = sticker_set.stickers[0].file_id
assert bot.set_sticker_position_in_set(file_id, 1)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_2_tgs(self, bot, animated_sticker_set):
file_id = animated_sticker_set.stickers[0].file_id
assert bot.set_sticker_position_in_set(file_id, 1)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_3(self, bot, sticker_set):
def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file):
sleep(1)
assert bot.set_sticker_set_thumb('test_by_{}'.format(bot.username), chat_id,
sticker_set_thumb_file)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_3_tgs(self, bot, chat_id, animated_sticker_file, animated_sticker_set):
sleep(1)
assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id,
animated_sticker_file)
file_id = animated_sticker_set.stickers[-1].file_id
# also test with file input and mask
assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id,
file_id)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_4_png(self, bot, sticker_set):
sleep(1)
file_id = sticker_set.stickers[-1].file_id
assert bot.delete_sticker_from_set(file_id)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_4_tgs(self, bot, animated_sticker_set):
sleep(1)
file_id = animated_sticker_set.stickers[-1].file_id
assert bot.delete_sticker_from_set(file_id)
def test_get_file_instance_method(self, monkeypatch, sticker):
def test(*args, **kwargs):
return args[1] == sticker.file_id