🔀 Merge master into fix-460

#494
This commit is contained in:
Jannes Höke 2017-02-27 14:33:58 +01:00
commit 2954ca2bad
30 changed files with 622 additions and 228 deletions

View file

@ -4,6 +4,7 @@ python:
- "3.3"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
branches:

View file

@ -12,6 +12,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Anton Tagunov <https://github.com/anton-tagunov>`_
- `Balduro <https://github.com/Balduro>`_
- `bimmlerd <https://github.com/bimmlerd>`_
- `daimajia <https://github.com/daimajia>`_
- `Eli Gao <https://github.com/eligao>`_
- `ErgoZ Riftbit Vaper <https://github.com/ergoz>`_
- `Eugene Lisitsky <https://github.com/lisitsky>`_

View file

@ -149,7 +149,7 @@ def main():
# Start the Bot
updater.start_polling()
# Run the bot until the you presses Ctrl-C or the process receives SIGINT,
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()

View file

@ -142,7 +142,7 @@ def main():
# Start the Bot
updater.start_polling()
# Run the bot until the you presses Ctrl-C or the process receives SIGINT,
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()

View file

@ -64,7 +64,7 @@ def main():
# Start the Bot
updater.start_polling()
# Run the bot until the you presses Ctrl-C or the process receives SIGINT,
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()

View file

@ -98,9 +98,9 @@ def main():
# Start the Bot
updater.start_polling()
# Block until the you presses Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
# Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or
# SIGABRT. This should be used most of the time, since start_polling() is
# non-blocking and will stop the bot gracefully.
updater.idle()

View file

@ -36,6 +36,7 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
install_requires=requirements(),
extras_require={
'json': 'ujson',
'socks': 'PySocks'
},
include_package_data=True,
classifiers=[
@ -53,4 +54,5 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
],)

View file

@ -158,9 +158,14 @@ class Bot(TelegramObject):
return decorator
@log
def getMe(self, **kwargs):
def getMe(self, timeout=None, **kwargs):
"""A simple method for testing your bot's auth token.
Args:
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
:class:`telegram.User`: A :class:`telegram.User` instance representing that bot if the
credentials are valid, `None` otherwise.
@ -171,7 +176,7 @@ class Bot(TelegramObject):
"""
url = '{0}/getMe'.format(self.base_url)
result = self._request.get(url)
result = self._request.get(url, timeout=timeout)
self.bot = User.de_json(result, self)
@ -211,8 +216,9 @@ class Bot(TelegramObject):
interface options. A JSON-serialized object for an inline
keyboard, custom reply keyboard, instructions to remove reply
keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as
the definitive timeout (in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -251,8 +257,9 @@ class Bot(TelegramObject):
message_id: Unique message identifier.
disable_notification (Optional[bool]): Sends the message silently. iOS users will not
receive a notification, Android users will receive a notification with no sound.
timeout (Optional[float]): If this value is specified, use it as
the definitive timeout (in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -301,8 +308,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -362,8 +370,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -406,7 +415,7 @@ class Bot(TelegramObject):
chat_id: Unique identifier for the message recipient - Chat id.
document: File to send. You can either pass a file_id as String to resend a file that
is already on the Telegram servers, or upload a new file using multipart/form-data.
filename (Optional[str]): File name that shows in telegram message (it is usefull when
filename (Optional[str]): File name that shows in telegram message (it is useful when
you send file generated by temp module, for example).
caption (Optional[str]): Document caption (may also be used when resending documents by
file_id), 0-200 characters.
@ -417,8 +426,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -463,8 +473,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -510,8 +521,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@ -563,8 +575,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -609,8 +622,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -658,8 +672,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -712,8 +727,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -758,8 +774,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options.
A JSON-serialized object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as
the definitive timeout (in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
:class:`telegram.Message`: On success, the sent message is returned.
@ -776,7 +793,7 @@ class Bot(TelegramObject):
@log
@message
def sendChatAction(self, chat_id, action, **kwargs):
def sendChatAction(self, chat_id, action, timeout=None, **kwargs):
"""Use this method when you need to tell the user that something is happening on the bot's
side. The status is set for 5 seconds or less (when a message arrives from your bot,
Telegram clients clear its typing status).
@ -792,6 +809,9 @@ class Bot(TelegramObject):
- ChatAction.UPLOAD_AUDIO for audio files,
- ChatAction.UPLOAD_DOCUMENT for general files,
- ChatAction.FIND_LOCATION for location data.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
"""
@ -833,8 +853,9 @@ class Bot(TelegramObject):
start message with the parameter switch_pm_parameter.
switch_pm_parameter (Optional[str]): Parameter for the start message sent to the bot
when user presses the switch button.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -875,8 +896,9 @@ class Bot(TelegramObject):
default, all photos are returned.
limit (Optional[int]): Limits the number of photos to be retrieved. Values between
1-100 are accepted. Defaults to 100.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -907,8 +929,9 @@ class Bot(TelegramObject):
Args:
file_id: File identifier to get info about.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -941,8 +964,9 @@ class Bot(TelegramObject):
chat_id: Unique identifier for the target group or username of the target supergroup
(in the format @supergroupusername).
user_id: Unique identifier of the target user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -970,8 +994,9 @@ class Bot(TelegramObject):
chat_id: Unique identifier for the target group or username of the target supergroup
(in the format @supergroupusername).
user_id: Unique identifier of the target user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1012,8 +1037,9 @@ class Bot(TelegramObject):
cache_time (Optional[int]): The maximum amount of time in seconds that the result of
the callback query may be cached client-side. Telegram apps will support caching
starting in version 3.14. Defaults to 0.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1069,8 +1095,9 @@ class Bot(TelegramObject):
reply_markup (Optional[:class:`telegram.ReplyMarkup`]): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1122,8 +1149,9 @@ class Bot(TelegramObject):
caption (Optional[str]): New caption of the message.
reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): A JSON-serialized
object for an inline keyboard.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1176,8 +1204,9 @@ class Bot(TelegramObject):
specified. Identifier of the inline message.
reply_markup (Optional[:class:`telegram.InlineKeyboardMarkup`]): A JSON-serialized
object for an inline keyboard.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1208,7 +1237,13 @@ class Bot(TelegramObject):
return url, data
@log
def getUpdates(self, offset=None, limit=100, timeout=0, network_delay=5., **kwargs):
def getUpdates(self,
offset=None,
limit=100,
timeout=0,
network_delay=None,
read_latency=2.,
**kwargs):
"""Use this method to receive incoming updates using long polling.
Args:
@ -1220,13 +1255,25 @@ class Bot(TelegramObject):
limit (Optional[int]): Limits the number of updates to be retrieved. Values between
1-100 are accepted. Defaults to 100.
timeout (Optional[int]): Timeout in seconds for long polling. Defaults to 0, i.e. usual
short polling.
network_delay (Optional[float]): Additional timeout in seconds to allow the response
from Telegram servers. This should cover network latency around the globe, SSL
handshake and slowness of the Telegram servers (which unfortunately happens a lot
recently - 2016-05-28). Defaults to 5.
short polling. Be careful not to set this timeout too high, as the connection might
be dropped and there's no way of knowing it immediately (so most likely the failure
will be detected after the timeout had passed).
network_delay: Deprecated. Will be honoured as `read_latency` for a while but will be
removed in the future.
read_latency (Optional[float|int]): Grace time in seconds for receiving the reply from
server. Will be added to the `timeout` value and used as the read timeout from
server (Default: 2).
**kwargs (dict): Arbitrary keyword arguments.
Notes:
The main problem with long polling is that a connection will be dropped and we won't
be getting the notification in time for it. For that, we need to use long polling, but
not too long as well read latency which is short, but not too short.
Long polling improves performance, but if it's too long and the connection is dropped
on many cases we won't know the connection dropped before the long polling timeout and
the read latency time had passed. If you experience connection timeouts, you should
tune these settings.
Returns:
list[:class:`telegram.Update`]
@ -1236,6 +1283,10 @@ class Bot(TelegramObject):
"""
url = '{0}/getUpdates'.format(self.base_url)
if network_delay is not None:
warnings.warn('network_delay is deprecated, use read_latency instead')
read_latency = network_delay
data = {'timeout': timeout}
if offset:
@ -1243,9 +1294,12 @@ class Bot(TelegramObject):
if limit:
data['limit'] = limit
urlopen_timeout = timeout + network_delay
result = self._request.post(url, data, timeout=urlopen_timeout)
# Ideally we'd use an aggressive read timeout for the polling. However,
# * Short polling should return within 2 seconds.
# * Long polling poses a different problem: the connection might have been dropped while
# waiting for the server to return and there's no way of knowing the connection had been
# dropped in real time.
result = self._request.post(url, data, timeout=float(read_latency) + float(timeout))
if result:
self.logger.debug('Getting updates: %s', [u['update_id'] for u in result])
@ -1263,9 +1317,10 @@ class Bot(TelegramObject):
Args:
webhook_url: HTTPS url to send updates to. Use an empty string to remove webhook
integration
timeout (Optional[float]): If this value is specified, use it as
the definitive timeout (in seconds) for urlopen() operations.
integration.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1295,8 +1350,9 @@ class Bot(TelegramObject):
Args:
chat_id: Unique identifier for the target chat or username of the target channel (in
the format @channelusername).
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1322,8 +1378,9 @@ class Bot(TelegramObject):
Args:
chat_id: Unique identifier for the target chat or username of the target channel (in
the format @channelusername).
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1352,8 +1409,9 @@ class Bot(TelegramObject):
Args:
chat_id: Unique identifier for the target chat or username of the target channel (in
the format @channelusername).
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1378,8 +1436,9 @@ class Bot(TelegramObject):
Args:
chat_id: Unique identifier for the target chat or username of the target channel (in
the format @channelusername).
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1405,8 +1464,9 @@ class Bot(TelegramObject):
chat_id: Unique identifier for the target chat or username of the target channel (in
the format @channelusername).
user_id: Unique identifier of the target user.
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
**kwargs (dict): Arbitrary keyword arguments.
Returns:
@ -1424,11 +1484,16 @@ class Bot(TelegramObject):
return ChatMember.de_json(result, self)
def getWebhookInfo(self, **kwargs):
def getWebhookInfo(self, timeout=None, **kwargs):
"""Use this method to get current webhook status.
If the bot is using getUpdates, will return an object with the url field empty.
Args:
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
:class: `telegram.WebhookInfo`
@ -1437,7 +1502,7 @@ class Bot(TelegramObject):
data = {}
result = self._request.post(url, data, timeout=kwargs.get('timeout'))
result = self._request.post(url, data, timeout=timeout)
return WebhookInfo.de_json(result, self)
@ -1450,6 +1515,7 @@ class Bot(TelegramObject):
edit_message=None,
force=None,
disable_edit_message=None,
timeout=None,
**kwargs):
"""Use this method to set the score of the specified user in a game.
@ -1469,10 +1535,9 @@ class Bot(TelegramObject):
automatically edited to include the current scoreboard.
edit_message (Optional[bool]): Deprecated. Has the opposite logic for
`disable_edit_message`.
Keyword Args:
timeout (Optional[float]): If this value is specified, use it as the definitive timeout
(in seconds) for urlopen() operations.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
:class:`telegram.Message` or True: The edited message, or if the
@ -1500,7 +1565,7 @@ class Bot(TelegramObject):
else:
warnings.warn('edit_message is ignored when disable_edit_message is used')
result = self._request.post(url, data, timeout=kwargs.get('timeout'))
result = self._request.post(url, data, timeout=timeout)
if result is True:
return result
else:
@ -1511,9 +1576,15 @@ class Bot(TelegramObject):
chat_id=None,
message_id=None,
inline_message_id=None,
timeout=None,
**kwargs):
"""Use this method to get data for high score tables.
Args:
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
list[:class:`telegram.GameHighScore`]: Scores of the specified user and several of his
neighbors in a game.
@ -1530,7 +1601,7 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
result = self._request.post(url, data, timeout=kwargs.get('timeout'))
result = self._request.post(url, data, timeout=timeout)
return [GameHighScore.de_json(hs, self) for hs in result]

View file

@ -32,7 +32,7 @@ class Chat(TelegramObject):
username (str): Username, for private chats and channels if available
first_name (str): First name of the other party in a private chat
last_name (str): Last name of the other party in a private chat
all_members_are_admins (bool): True if a group has 'All Members Are Admins' enabled.
all_members_are_administrators (bool): True if group has 'All Members Are Administrators'
Args:
id (int):
@ -57,7 +57,7 @@ class Chat(TelegramObject):
username=None,
first_name=None,
last_name=None,
all_members_are_admins=None,
all_members_are_administrators=None,
bot=None,
**kwargs):
# Required
@ -68,7 +68,7 @@ class Chat(TelegramObject):
self.username = username
self.first_name = first_name
self.last_name = last_name
self.all_members_are_admins = all_members_are_admins
self.all_members_are_administrators = all_members_are_administrators
self.bot = bot

View file

@ -83,8 +83,14 @@ class CommandHandler(Handler):
and (update.message or update.edited_message and self.allow_edited)):
message = update.message or update.edited_message
return (message.text and message.text.startswith('/')
and message.text[1:].split(' ')[0].split('@')[0] == self.command)
if message.text:
command = message.text[1:].split(' ')[0].split('@')
command.append(
update.message.bot.username) # in case the command was send without a username
return (message.text.startswith('/') and command[0] == self.command
and command[1] == update.message.bot.username)
else:
return False
else:
return False

View file

@ -22,7 +22,6 @@ import logging
from telegram import Update
from telegram.ext import Handler
from telegram.utils.helpers import extract_chat_and_user
from telegram.utils.promise import Promise
@ -119,7 +118,7 @@ class ConversationHandler(Handler):
if not isinstance(update, Update) or update.channel_post:
return False
chat, user = extract_chat_and_user(update)
chat, user = update.extract_chat_and_user()
key = (chat.id, user.id) if chat else (None, user.id)
state = self.conversations.get(key)

View file

@ -20,7 +20,6 @@
Dispatcher """
from telegram.utils.deprecate import deprecate
from telegram.utils.helpers import extract_chat_and_user
class Handler(object):
@ -105,7 +104,7 @@ class Handler(object):
if self.pass_job_queue:
optional_args['job_queue'] = dispatcher.job_queue
if self.pass_user_data or self.pass_chat_data:
chat, user = extract_chat_and_user(update)
chat, user = update.extract_chat_and_user()
if self.pass_user_data:
optional_args['user_data'] = dispatcher.user_data[user.id]

View file

@ -22,6 +22,7 @@ Telegram bots intuitive."""
import logging
import os
import ssl
import warnings
from threading import Thread, Lock, current_thread, Event
from time import sleep
import subprocess
@ -61,6 +62,8 @@ class Updater(object):
bot (Optional[Bot]): A pre-initialized bot instance. If a pre-initizlied bot is used, it is
the user's responsibility to create it using a `Request` instance with a large enough
connection pool.
request_kwargs (Optional[dict]): Keyword args to control the creation of a request object
(ignored if `bot` argument is used).
Raises:
ValueError: If both `token` and `bot` are passed or none of them.
@ -68,7 +71,7 @@ class Updater(object):
"""
_request = None
def __init__(self, token=None, base_url=None, workers=4, bot=None):
def __init__(self, token=None, base_url=None, workers=4, bot=None, request_kwargs=None):
if (token is None) and (bot is None):
raise ValueError('`token` or `bot` must be passed')
if (token is not None) and (bot is not None):
@ -83,7 +86,11 @@ class Updater(object):
# * 1 for polling Updater (even if webhook is used, we can spare a connection)
# * 1 for JobQueue
# * 1 for main thread
self._request = Request(con_pool_size=workers + 4)
if request_kwargs is None:
request_kwargs = {}
if 'con_pool_size' not in request_kwargs:
request_kwargs['con_pool_size'] = workers + 4
self._request = Request(**request_kwargs)
self.bot = Bot(token, base_url, request=self._request)
self.update_queue = Queue()
self.job_queue = JobQueue(self.bot)
@ -122,9 +129,10 @@ class Updater(object):
def start_polling(self,
poll_interval=0.0,
timeout=10,
network_delay=5.,
network_delay=None,
clean=False,
bootstrap_retries=0):
bootstrap_retries=0,
read_latency=2.):
"""
Starts polling updates from Telegram.
@ -134,7 +142,8 @@ class Updater(object):
timeout (Optional[float]): Passed to Bot.getUpdates
network_delay (Optional[float]): Passed to Bot.getUpdates
network_delay: Deprecated. Will be honoured as `read_latency` for a while but will be
removed in the future.
clean (Optional[bool]): Whether to clean any pending updates on Telegram servers before
actually starting to poll. Default is False.
@ -143,13 +152,22 @@ class Updater(object):
will retry on failures on the Telegram server.
| < 0 - retry indefinitely
| 0 - no retries (default)
| 0 - no retries (default)
| > 0 - retry up to X times
read_latency (Optional[float|int]): Grace time in seconds for receiving the reply from
server. Will be added to the `timeout` value and used as the read timeout from
server (Default: 2).
Returns:
Queue: The update queue that can be filled from the main thread
"""
if network_delay is not None:
warnings.warn('network_delay is deprecated, use read_latency instead')
read_latency = network_delay
with self.__lock:
if not self.running:
self.running = True
@ -158,7 +176,7 @@ class Updater(object):
self.job_queue.start()
self._init_thread(self.dispatcher.start, "dispatcher")
self._init_thread(self._start_polling, "updater", poll_interval, timeout,
network_delay, bootstrap_retries, clean)
read_latency, bootstrap_retries, clean)
# Return the update queue so the main thread can insert updates
return self.update_queue
@ -215,7 +233,7 @@ class Updater(object):
# Return the update queue so the main thread can insert updates
return self.update_queue
def _start_polling(self, poll_interval, timeout, network_delay, bootstrap_retries, clean):
def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean):
"""
Thread target of thread 'updater'. Runs in background, pulls
updates from Telegram and inserts them in the update queue of the
@ -230,7 +248,7 @@ class Updater(object):
while self.running:
try:
updates = self.bot.getUpdates(
self.last_update_id, timeout=timeout, network_delay=network_delay)
self.last_update_id, timeout=timeout, read_latency=read_latency)
except RetryAfter as e:
self.logger.info(str(e))
cur_interval = 0.5 + e.retry_after

View file

@ -66,20 +66,24 @@ class File(TelegramObject):
return File(bot=bot, **data)
def download(self, custom_path=None, out=None):
def download(self, custom_path=None, out=None, timeout=None):
"""
Download this file. By default, the file is saved in the current working directory with its
original filename as reported by Telegram. If a ``custom_path`` is supplied, it will be
saved to that path instead. If ``out`` is defined, the file contents will be saved to that
object using the ``out.write`` method. ``custom_path`` and ``out`` are mutually exclusive.
Keyword Args:
Args:
custom_path (Optional[str]): Custom path.
out (Optional[object]): A file-like object. Must be opened in binary mode, if
applicable.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Raises:
ValueError: If both ``custom_path`` and ``out`` are passed.
"""
if custom_path is not None and out is not None:
@ -97,4 +101,4 @@ class File(TelegramObject):
else:
filename = basename(url)
self.bot.request.download(url, filename)
self.bot.request.download(url, filename, timeout=timeout)

View file

@ -18,7 +18,6 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Message."""
import sys
from datetime import datetime
from time import mktime
@ -586,6 +585,7 @@ class Message(TelegramObject):
Returns:
dict[:class:`telegram.MessageEntity`, ``str``]: A dictionary of entities mapped to the
text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
@ -594,3 +594,80 @@ class Message(TelegramObject):
entity: self.parse_entity(entity)
for entity in self.entities if entity.type in types
}
@property
def text_html(self):
"""
Creates an html-formatted string from the markup entities found in the message
(uses ``parse_entities``).
Use this if you want to retrieve the original string sent by the bot, as opposed to the
plain text with corresponding markup entities.
Returns:
str
"""
entities = self.parse_entities()
message_text = self.text
markdown_text = ''
last_offset = 0
for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)):
if entity.type == MessageEntity.TEXT_LINK:
insert = '<a href="{}">{}</a>'.format(entity.url, text)
elif entity.type == MessageEntity.BOLD:
insert = '<b>' + text + '</b>'
elif entity.type == MessageEntity.ITALIC:
insert = '<i>' + text + '</i>'
elif entity.type == MessageEntity.CODE:
insert = '<code>' + text + '</code>'
elif entity.type == MessageEntity.PRE:
insert = '<pre>' + text + '</pre>'
else:
insert = text
markdown_text += message_text[last_offset:entity.offset] + insert
last_offset = entity.offset + entity.length
markdown_text += message_text[last_offset:]
return markdown_text
@property
def text_markdown(self):
"""
Creates a markdown-formatted string from the markup entities found in the message
(uses ``parse_entities``).
Use this if you want to retrieve the original string sent by the bot, as opposed to the
plain text with corresponding markup entities.
Returns:
str
"""
entities = self.parse_entities()
message_text = self.text
markdown_text = ''
last_offset = 0
for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)):
if entity.type == MessageEntity.TEXT_LINK:
insert = '[{}]({})'.format(text, entity.url)
elif entity.type == MessageEntity.BOLD:
insert = '*' + text + '*'
elif entity.type == MessageEntity.ITALIC:
insert = '_' + text + '_'
elif entity.type == MessageEntity.CODE:
insert = '`' + text + '`'
elif entity.type == MessageEntity.PRE:
insert = '```' + text + '```'
else:
insert = text
markdown_text += message_text[last_offset:entity.offset] + insert
last_offset = entity.offset + entity.length
markdown_text += message_text[last_offset:]
return markdown_text

View file

@ -98,3 +98,80 @@ class Update(TelegramObject):
data['edited_channel_post'] = Message.de_json(data.get('edited_channel_post'), bot)
return Update(**data)
def extract_chat_and_user(self):
"""
Helper method to get the sender's chat and user objects from an arbitrary update.
Depending on the type of update, one of the available attributes ``message``,
``edited_message`` or ``callback_query`` is used to determine the result.
Returns:
tuple: of (chat, user), with None-values if no object could not be found.
"""
user = None
chat = None
if self.message:
user = self.message.from_user
chat = self.message.chat
elif self.edited_message:
user = self.edited_message.from_user
chat = self.edited_message.chat
elif self.inline_query:
user = self.inline_query.from_user
elif self.chosen_inline_result:
user = self.chosen_inline_result.from_user
elif self.callback_query:
user = self.callback_query.from_user
chat = self.callback_query.message.chat if self.callback_query.message else None
return chat, user
def extract_message_text(self):
"""
Helper method to get the message text from an arbitrary update.
Depending on the type of update, one of the available attributes ``message``,
``edited_message`` or ``callback_query`` is used to determine the result.
Returns:
str: The extracted message text
Raises:
ValueError: If no message text was found in the update
"""
if self.message:
return self.message.text
elif self.edited_message:
return self.edited_message.text
elif self.callback_query:
return self.callback_query.message.text
else:
raise ValueError("Update contains no message text.")
def extract_entities(self):
"""
Helper method to get parsed entities from an arbitrary update.
Depending on the type of update, one of the available attributes ``message``,
``edited_message`` or ``callback_query`` is used to determine the result.
Returns:
dict[:class:`telegram.MessageEntity`, ``str``]: A dictionary of entities mapped to the
text that belongs to them, calculated based on UTF-16 codepoints.
Raises:
ValueError: If no entities were found in the update
"""
if self.message:
return self.message.parse_entities()
elif self.edited_message:
return self.edited_message.parse_entities()
elif self.callback_query:
return self.callback_query.message.parse_entities()
else:
raise ValueError("No message object found in self, therefore no entities available.")

View file

@ -18,27 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
""" This module contains helper functions """
import re
def extract_chat_and_user(update):
user = None
chat = None
if update.message:
user = update.message.from_user
chat = update.message.chat
elif update.edited_message:
user = update.edited_message.from_user
chat = update.edited_message.chat
elif update.inline_query:
user = update.inline_query.from_user
elif update.chosen_inline_result:
user = update.chosen_inline_result.from_user
elif update.callback_query:
user = update.callback_query.from_user
chat = update.callback_query.message.chat if update.callback_query.message else None
return chat, user
def escape_markdown(text):
"""Helper function to escape telegram markup symbols"""
escape_chars = '\*_`\['
return re.sub(r'([%s])' % escape_chars, r'\\\1', text)

View file

@ -17,18 +17,23 @@
# 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 methods to make POST and GET requests"""
import os
import socket
import logging
try:
import ujson as json
except ImportError:
import json
import os
import socket
import logging
import certifi
import urllib3
from urllib3.connection import HTTPConnection
from urllib3.util.timeout import Timeout
try:
from urllib3.contrib.socks import SOCKSProxyManager
except ImportError:
SOCKSProxyManager = None
from telegram import (InputFile, TelegramError)
from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated,
@ -43,23 +48,40 @@ class Request(object):
telegram servers.
Args:
con_pool_size (int): Number of connections to keep in the connection pool.
proxy_url (str): The URL to the proxy server. For example: `http://127.0.0.1:3128`.
urllib3_proxy_kwargs (dict): Arbitrary arguments passed as-is to `urllib3.ProxyManager`.
This value will be ignored if proxy_url is not set.
connect_timeout (int|float): The maximum amount of time (in seconds) to wait for a
connection attempt to a server to succeed. None will set an infinite timeout for
connection attempts. (default: 5.)
read_timeout (int|float): The maximum amount of time (in seconds) to wait between
consecutive read operations for a response from the server. None will set an infinite
timeout. This value is usually overridden by the various ``telegram.Bot`` methods.
(default: 5.)
"""
def __init__(self, con_pool_size=1, proxy_url=None, urllib3_proxy_kwargs=None):
def __init__(self,
con_pool_size=1,
proxy_url=None,
urllib3_proxy_kwargs=None,
connect_timeout=5.,
read_timeout=5.):
if urllib3_proxy_kwargs is None:
urllib3_proxy_kwargs = dict()
self._connect_timeout = connect_timeout
kwargs = dict(
maxsize=con_pool_size,
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where(),
socket_options=HTTPConnection.default_socket_options + [
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
])
],
timeout=urllib3.Timeout(
connect=self._connect_timeout, read=read_timeout),)
# Set a proxy according to the following order:
# * proxy defined in proxy_url (+ urllib3_proxy_kwargs)
@ -74,11 +96,16 @@ class Request(object):
mgr = urllib3.PoolManager(**kwargs)
else:
kwargs.update(urllib3_proxy_kwargs)
mgr = urllib3.proxy_from_url(proxy_url, **kwargs)
if mgr.proxy.auth:
# TODO: what about other auth types?
auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth)
mgr.proxy_headers.update(auth_hdrs)
if proxy_url.startswith('socks'):
if not SOCKSProxyManager:
raise RuntimeError('PySocks is missing')
mgr = SOCKSProxyManager(proxy_url, **kwargs)
else:
mgr = urllib3.proxy_from_url(proxy_url, **kwargs)
if mgr.proxy.auth:
# TODO: what about other auth types?
auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth)
mgr.proxy_headers.update(auth_hdrs)
self._con_pool = mgr
@ -128,6 +155,12 @@ class Request(object):
TelegramError
"""
# Make sure to hint Telegram servers that we reuse connections by sending
# "Connection: keep-alive" in the HTTP headers.
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['connection'] = 'keep-alive'
try:
resp = self._con_pool.request(*args, **kwargs)
except urllib3.exceptions.TimeoutError:
@ -149,7 +182,7 @@ class Request(object):
if resp.status in (401, 403):
raise Unauthorized()
elif resp.status == 400:
raise BadRequest(repr(message))
raise BadRequest(message)
elif resp.status == 404:
raise InvalidToken()
elif resp.status == 502:
@ -157,33 +190,14 @@ class Request(object):
else:
raise NetworkError('{0} ({1})'.format(message, resp.status))
def get(self, url):
def get(self, url, timeout=None):
"""Request an URL.
Args:
url:
The web location we want to retrieve.
Returns:
A JSON object.
"""
result = self._request_wrapper('GET', url)
return self._parse(result)
def post(self, url, data, timeout=None):
"""Request an URL.
Args:
url:
The web location we want to retrieve.
data:
A dict of (str, unicode) key/value pairs.
timeout:
float. If this value is specified, use it as the definitive timeout (in
seconds) for urlopen() operations. [Optional]
Notes:
If neither `timeout` nor `data['timeout']` is specified. The underlying
defaults are used.
url (str): The web location we want to retrieve.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
A JSON object.
@ -192,15 +206,33 @@ class Request(object):
urlopen_kwargs = {}
if timeout is not None:
urlopen_kwargs['timeout'] = timeout
urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout)
result = self._request_wrapper('GET', url, **urlopen_kwargs)
return self._parse(result)
def post(self, url, data, timeout=None):
"""Request an URL.
Args:
url (str): The web location we want to retrieve.
data (dict[str, str]): A dict of key/value pairs. Note: On py2.7 value is unicode.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
Returns:
A JSON object.
"""
urlopen_kwargs = {}
if timeout is not None:
urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout)
if InputFile.is_inputfile(data):
data = InputFile(data)
result = self._request_wrapper('POST',
url,
body=data.to_form(),
headers=data.headers,
**urlopen_kwargs)
result = self._request_wrapper(
'POST', url, body=data.to_form(), headers=data.headers, **urlopen_kwargs)
else:
data = json.dumps(data)
result = self._request_wrapper(
@ -212,24 +244,34 @@ class Request(object):
return self._parse(result)
def retrieve(self, url):
def retrieve(self, url, timeout=None):
"""Retrieve the contents of a file by its URL.
Args:
url:
The web location we want to retrieve.
"""
return self._request_wrapper('GET', url)
def download(self, url, filename):
Args:
url (str): The web location we want to retrieve.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
"""
urlopen_kwargs = {}
if timeout is not None:
urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout)
return self._request_wrapper('GET', url, **urlopen_kwargs)
def download(self, url, filename, timeout=None):
"""Download a file by its URL.
Args:
url:
The web location we want to retrieve.
url (str): The web location we want to retrieve.
timeout (Optional[int|float]): If this value is specified, use it as the read timeout
from the server (instead of the one specified during creation of the connection
pool).
filename:
The filename within the path to download the file.
"""
buf = self.retrieve(url)
buf = self.retrieve(url, timeout=timeout)
with open(filename, 'wb') as fobj:
fobj.write(buf)

5
tests/README.md Normal file
View file

@ -0,0 +1,5 @@
tests
=====
Some tests fail because of weird behaviour of the Telegram API. We comment these
out and mark them with a `TODO` comment.

View file

@ -19,8 +19,8 @@
"""This module contains an object that represents a Base class for tests"""
import os
import sys
import signal
import sys
from nose.tools import make_decorator
@ -78,13 +78,21 @@ def timeout(time_limit):
raise TestTimedOut(time_limit, frame)
def newfunc(*args, **kwargs):
orig_handler = signal.signal(signal.SIGALRM, timed_out)
signal.alarm(time_limit)
try:
# Will only work on unix systems
orig_handler = signal.signal(signal.SIGALRM, timed_out)
signal.alarm(time_limit)
except AttributeError:
pass
try:
rc = func(*args, **kwargs)
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, orig_handler)
try:
# Will only work on unix systems
signal.alarm(0)
signal.signal(signal.SIGALRM, orig_handler)
except AttributeError:
pass
return rc
newfunc = make_decorator(func)(newfunc)

View file

@ -35,7 +35,7 @@ class AudioTest(BaseTest, unittest.TestCase):
def setUp(self):
self.audio_file = open('tests/data/telegram.mp3', 'rb')
self.audio_file_id = 'BQADAQADDwADHyP1B6PSPq2HjX8kAg'
self.audio_file_id = 'CQADAQADDwADHyP1B6PSPq2HjX8kAg'
self.audio_file_url = 'https://raw.githubusercontent.com/python-telegram-bot/python-telegram-bot/master/tests/data/telegram.mp3'
self.duration = 4
self.performer = 'Leandro Toledo'

View file

@ -182,7 +182,7 @@ class BotTest(BaseTest, unittest.TestCase):
self.assertTrue(self.is_json(message.to_json()))
self.assertEqual(message.game.description, 'This is a test game for python-telegram-bot.')
self.assertEqual(message.game.animation.file_id, 'BQADAQADKwIAAvjAuQABozciVqhFDO0C')
self.assertEqual(message.game.animation.file_id, 'CgADAQADKwIAAvjAuQABozciVqhFDO0C')
self.assertEqual(message.game.photo[0].file_size, 851)
@flaky(3, 1)
@ -365,7 +365,7 @@ class BotTest(BaseTest, unittest.TestCase):
chat_id=game.chat_id,
message_id=game.message_id)
self.assertTrue('BOT_SCORE_NOT_MODIFIED' in cm.exception.message)
self.assertTrue('BOT_SCORE_NOT_MODIFIED' in str(cm.exception.message).upper())
@flaky(3, 1)
@timeout(10)

View file

@ -36,13 +36,13 @@ class ChatTest(BaseTest, unittest.TestCase):
self.id = -28767330
self.title = 'ToledosPalaceBot - Group'
self.type = 'group'
self.all_members_are_admins = False
self.all_members_are_administrators = False
self.json_dict = {
'id': self.id,
'title': self.title,
'type': self.type,
'all_members_are_admins': self.all_members_are_admins
'all_members_are_administrators': self.all_members_are_administrators
}
def test_group_chat_de_json_empty_json(self):
@ -56,7 +56,8 @@ class ChatTest(BaseTest, unittest.TestCase):
self.assertEqual(group_chat.id, self.id)
self.assertEqual(group_chat.title, self.title)
self.assertEqual(group_chat.type, self.type)
self.assertEqual(group_chat.all_members_are_admins, self.all_members_are_admins)
self.assertEqual(group_chat.all_members_are_administrators,
self.all_members_are_administrators)
def test_group_chat_to_json(self):
group_chat = telegram.Chat.de_json(self.json_dict, self._bot)
@ -70,7 +71,8 @@ class ChatTest(BaseTest, unittest.TestCase):
self.assertEqual(group_chat['id'], self.id)
self.assertEqual(group_chat['title'], self.title)
self.assertEqual(group_chat['type'], self.type)
self.assertEqual(group_chat['all_members_are_admins'], self.all_members_are_admins)
self.assertEqual(group_chat['all_members_are_administrators'],
self.all_members_are_administrators)
@flaky(3, 1)
def test_send_action(self):

View file

@ -80,8 +80,8 @@ class ConversationHandlerTest(BaseTest, unittest.TestCase):
self.fallbacks = [CommandHandler('eat', self.start)]
def _setup_updater(self, *args, **kwargs):
bot = MockBot(*args, **kwargs)
self.updater = Updater(workers=2, bot=bot)
self.bot = MockBot(*args, **kwargs)
self.updater = Updater(workers=2, bot=self.bot)
def tearDown(self):
if self.updater is not None:
@ -137,32 +137,32 @@ class ConversationHandlerTest(BaseTest, unittest.TestCase):
queue = self.updater.start_polling(0.01)
# User one, starts the state machine.
message = Message(0, user, None, None, text="/start")
message = Message(0, user, None, None, text="/start", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertTrue(self.current_state[user.id] == self.THIRSTY)
# The user is thirsty and wants to brew coffee.
message = Message(0, user, None, None, text="/brew")
message = Message(0, user, None, None, text="/brew", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertTrue(self.current_state[user.id] == self.BREWING)
# Lets see if an invalid command makes sure, no state is changed.
message = Message(0, user, None, None, text="/nothing")
message = Message(0, user, None, None, text="/nothing", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertTrue(self.current_state[user.id] == self.BREWING)
# Lets see if the state machine still works by pouring coffee.
message = Message(0, user, None, None, text="/pourCoffee")
message = Message(0, user, None, None, text="/pourCoffee", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertTrue(self.current_state[user.id] == self.DRINKING)
# Let's now verify that for another user, who did not start yet,
# the state has not been changed.
message = Message(0, second_user, None, None, text="/brew")
message = Message(0, second_user, None, None, text="/brew", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertRaises(KeyError, self._get_state, user_id=second_user.id)
@ -197,13 +197,13 @@ class ConversationHandlerTest(BaseTest, unittest.TestCase):
# User starts the state machine with an async function that immediately ends the
# conversation. Async results are resolved when the users state is queried next time.
message = Message(0, user, None, None, text="/start")
message = Message(0, user, None, None, text="/start", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
# Assert that the Promise has been accepted as the new state
self.assertEquals(len(handler.conversations), 1)
message = Message(0, user, None, None, text="resolve promise pls")
message = Message(0, user, None, None, text="resolve promise pls", bot=self.bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
# Assert that the Promise has been resolved and the conversation ended.

42
tests/test_helpers.py Normal file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2016
# 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 General 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents Tests for Telegram
MessageEntity"""
import sys
import unittest
from telegram.utils import helpers
sys.path.append('.')
from tests.base import BaseTest
class HelpersTest(BaseTest, unittest.TestCase):
"""This object represents Tests for the Helpers Module"""
def test_escape_markdown(self):
test_str = "*bold*, _italic_, `code`, [text_link](http://github.com/)"
expected_str = "\*bold\*, \_italic\_, \`code\`, \[text\_link](http://github.com/)"
self.assertEquals(expected_str, helpers.escape_markdown(test_str))
if __name__ == '__main__':
unittest.main()

View file

@ -208,9 +208,7 @@ class JobQueueTest(BaseTest, unittest.TestCase):
def test_time_unit_dt_time_today(self):
# Testing running at a specific time today
delta = 2
current_time = datetime.datetime.now().time()
next_t = datetime.time(current_time.hour, current_time.minute, current_time.second + delta,
current_time.microsecond)
next_t = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + delta
self.jq.put(Job(self.job5, repeat=False), next_t=next_t)
@ -221,9 +219,7 @@ class JobQueueTest(BaseTest, unittest.TestCase):
# Testing running at a specific time that has passed today. Since we can't wait a day, we
# test if the jobs next_t has been calculated correctly
delta = -2
current_time = datetime.datetime.now().time()
next_t = datetime.time(current_time.hour, current_time.minute, current_time.second + delta,
current_time.microsecond)
next_t = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + delta + 60 * 60 * 24
self.jq.put(Job(self.job5, repeat=False), next_t=next_t)
@ -247,10 +243,7 @@ class JobQueueTest(BaseTest, unittest.TestCase):
def test_run_daily(self):
delta = 1
current_time = datetime.datetime.now().time()
time_of_day = datetime.time(current_time.hour, current_time.minute,
current_time.second + delta, current_time.microsecond)
time_of_day = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + 60 * 60 * 24 + delta
self.jq.run_daily(self.job1, time_of_day)

View file

@ -33,6 +33,45 @@ from tests.base import BaseTest
class MessageTest(BaseTest, unittest.TestCase):
"""This object represents Tests for Telegram MessageTest."""
def setUp(self):
self.test_entities = [
{
'length': 4,
'offset': 9,
'type': 'bold'
},
{
'length': 6,
'offset': 15,
'type': 'italic'
},
{
'length': 4,
'offset': 23,
'type': 'code'
},
{
'length': 5,
'offset': 29,
'type': 'text_link',
'url': 'http://github.com/'
},
{
'length': 3,
'offset': 39,
'type': 'pre'
},
]
self.test_text = 'Test for bold, italic, code, links and pre.'
self.test_message = telegram.Message(
message_id=1,
from_user=None,
date=None,
chat=None,
text=self.test_text,
entities=[telegram.MessageEntity(**e) for e in self.test_entities])
def test_parse_entity(self):
text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape')
@ -59,6 +98,17 @@ class MessageTest(BaseTest, unittest.TestCase):
{entity: 'http://google.com',
entity_2: 'h'})
def test_text_html(self):
test_html_string = 'Test for <b>bold</b>, <i>italic</i>, <code>code</code>, ' \
'<a href="http://github.com/">links</a> and <pre>pre</pre>.'
text_html = self.test_message.text_html
self.assertEquals(test_html_string, text_html)
def test_text_markdown(self):
test_md_string = 'Test for *bold*, _italic_, `code`, [links](http://github.com/) and ```pre```.'
text_markdown = self.test_message.text_markdown
self.assertEquals(test_md_string, text_markdown)
@flaky(3, 1)
def test_reply_text(self):
"""Test for Message.reply_text"""

View file

@ -35,7 +35,7 @@ class StickerTest(BaseTest, unittest.TestCase):
"""This object represents Tests for Telegram Sticker."""
def setUp(self):
self.sticker_file_id = 'BQADAQADHAADyIsGAAFZfq1bphjqlgI'
self.sticker_file_id = 'CAADAQADHAADyIsGAAFZfq1bphjqlgI'
self.width = 510
self.height = 512
self.thumb = {
@ -76,7 +76,7 @@ class StickerTest(BaseTest, unittest.TestCase):
self.assertEqual(sticker.emoji, self.emoji.decode('utf-8'))
else:
self.assertEqual(sticker.emoji, self.emoji)
self.assertEqual(sticker.file_size, self.file_size)
# self.assertEqual(sticker.file_size, self.file_size) # TODO
def test_sticker_de_json(self):
sticker = telegram.Sticker.de_json(self.json_dict, self._bot)

View file

@ -76,6 +76,17 @@ class UpdateTest(BaseTest, unittest.TestCase):
self.assertEqual(update['update_id'], self.update_id)
self.assertTrue(isinstance(update['message'], telegram.Message))
def test_extract_chat_and_user(self):
update = telegram.Update.de_json(self.json_dict, self._bot)
chat, user = update.extract_chat_and_user()
self.assertEqual(update.message.chat, chat)
self.assertEqual(update.message.from_user, user)
def test_extract_message_text(self):
update = telegram.Update.de_json(self.json_dict, self._bot)
text = update.extract_message_text()
self.assertEqual(update.message.text, text)
if __name__ == '__main__':
unittest.main()

View file

@ -231,31 +231,33 @@ class UpdaterTest(BaseTest, unittest.TestCase):
self.assertTrue(None is self.received_message)
def test_addRemoveTelegramCommandHandler(self):
self._setup_updater('/test')
self._setup_updater('', messages=0)
d = self.updater.dispatcher
handler = CommandHandler('test', self.telegramHandlerTest)
self.updater.dispatcher.add_handler(handler)
self.updater.start_polling(0.01)
user = User(first_name="singelton", id=404)
bot = self.updater.bot
queue = self.updater.start_polling(0.01)
# regular use
message = Message(0, user, None, None, text="/test", bot=bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertEqual(self.received_message, '/test')
# Remove handler
d.remove_handler(handler)
self.reset()
# assigned use
message = Message(0, user, None, None, text="/test@MockBot", bot=bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertEqual(self.received_message, '/test@MockBot')
self.updater.bot.send_messages = 1
# directed at other bot
self.reset()
message = Message(0, user, None, None, text="/test@OtherBot", bot=bot)
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertTrue(None is self.received_message)
def test_editedCommandHandler(self):
self._setup_updater('/test', edited=True)
d = self.updater.dispatcher
handler = CommandHandler('test', self.telegramHandlerEditedTest, allow_edited=True)
d.addHandler(handler)
self.updater.start_polling(0.01)
sleep(.1)
self.assertEqual(self.received_message, '/test')
# Remove handler
d.removeHandler(handler)
handler = CommandHandler('test', self.telegramHandlerEditedTest, allow_edited=False)
@ -788,9 +790,10 @@ class MockBot(object):
self.bootstrap_attempts = 0
self.bootstrap_err = bootstrap_err
self.edited = edited
self.username = "MockBot"
def mockUpdate(self, text):
message = Message(0, User(0, 'Testuser'), None, Chat(0, Chat.GROUP))
message = Message(0, User(0, 'Testuser'), None, Chat(0, Chat.GROUP), bot=self)
message.text = text
update = Update(0)
@ -809,7 +812,7 @@ class MockBot(object):
self.bootstrap_attempts += 1
raise self.bootstrap_err
def getUpdates(self, offset=None, limit=100, timeout=0, network_delay=2.):
def getUpdates(self, offset=None, limit=100, timeout=0, network_delay=None, read_latency=2.):
if self.raise_error:
raise TelegramError('Test Error 2')