mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-20 16:15:28 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38a33581b1 | |||
| fe821c08e6 | |||
| 0a9f4bfbdd | |||
| c4364c7166 | |||
| d63e710784 | |||
| f379f54d5a | |||
| bdf0cb91f3 | |||
| 3101ea8432 | |||
| beb8ba3db0 | |||
| f0b1aeb6fd | |||
| d65558888e | |||
| 61a66a32c8 | |||
| 392d4e1a9c | |||
| 9cb34af65a | |||
| e9cb6675ca | |||
| 982f6707e1 | |||
| d55d981e22 | |||
| f20953f7a9 | |||
| e18220be10 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.BotCommand
|
||||
===================
|
||||
|
||||
.. autoclass:: telegram.BotCommand
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.ext.PollHandler
|
||||
========================
|
||||
|
||||
.. autoclass:: telegram.ext.PollHandler
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 bot’s 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 bot’s 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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::
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user