Merge remote-tracking branch 'origin/master' into urllib3-vendor-beta

This commit is contained in:
Noam Meltzer 2017-04-29 14:09:45 +03:00
commit a6b28b022a
27 changed files with 698 additions and 134 deletions

View file

@ -85,6 +85,12 @@ Here's how to make a one-off code change.
$ make test
If you don't have ``make``, do:
.. code-block::
$ nosetests -v
- To actually make the commit (this will trigger tests for yapf, lint and pep8 automatically):
.. code-block:: bash

View file

@ -19,6 +19,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>`_
- `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_
- `Eli Gao <https://github.com/eligao>`_
- `ErgoZ Riftbit Vaper <https://github.com/ergoz>`_
@ -28,6 +29,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Jacob Bom <https://github.com/bomjacob>`_
- `JASON0916 <https://github.com/JASON0916>`_
- `jh0ker <https://github.com/jh0ker>`_
- `John Yong <https://github.com/whipermr5>`_
- `jossalgon <https://github.com/jossalgon>`_
- `JRoot3D <https://github.com/JRoot3D>`_
- `jlmadurga <https://github.com/jlmadurga>`_
@ -36,6 +38,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `macrojames <https://github.com/macrojames>`_
- `Michael Elovskikh <https://github.com/wronglink>`_
- `naveenvhegde <https://github.com/naveenvhegde>`_
- `neurrone <https://github.com/neurrone>`_
- `njittam <https://github.com/njittam>`_
- `Noam Meltzer <https://github.com/tsnoam>`_
- `Oleg Shlyazhko <https://github.com/ollmer>`_
@ -46,6 +49,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Joscha Götzer <https://github.com/Rostgnom>`_
- `Shelomentsev D <https://github.com/shelomentsevd>`_
- `sooyhwang <https://github.com/sooyhwang>`_
- `thodnev <https://github.com/thodnev>`_
- `Valentijn <https://github.com/Faalentijn>`_
- `voider1 <https://github.com/voider1>`_
- `wjt <https://github.com/wjt>`_

View file

@ -2,6 +2,12 @@
Changes
=======
**2017-04-17**
*Released 5.3.1*
- Hotfix release due to bug introduced by urllib3 version 1.21
**2016-12-11**
*Released 5.3*

View file

@ -61,7 +61,7 @@ author = u'Leandro Toledo'
# The short X.Y version.
version = '5.3' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '5.3.0' # telegram.__version__
release = '5.3.1' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -0,0 +1,7 @@
telegram.ext.messagequeue module
================================
.. automodule:: telegram.ext.messagequeue
:members:
:undoc-members:
:show-inheritance:

View file

@ -16,6 +16,7 @@ Submodules
telegram.ext.commandhandler
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.messagequeue
telegram.ext.filters
telegram.ext.regexhandler
telegram.ext.stringcommandhandler

View file

@ -32,6 +32,7 @@ Attributes:
limit, but eventually you'll begin receiving 429 errors.
MAX_MESSAGES_PER_SECOND (int)
MAX_MESSAGES_PER_MINUTE_PER_GROUP (int)
MAX_INLINE_QUERY_RESULTS (int)
The following constant have been found by experimentation:
@ -52,3 +53,4 @@ MAX_MESSAGES_PER_SECOND_PER_CHAT = 1
MAX_MESSAGES_PER_SECOND = 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20
MAX_MESSAGE_ENTITIES = 100
MAX_INLINE_QUERY_RESULTS = 50

View file

@ -23,7 +23,6 @@ import re
from future.utils import string_types
from telegram import Update
from telegram.utils.deprecate import deprecate
from .handler import Handler
@ -105,8 +104,3 @@ class CallbackQueryHandler(Handler):
optional_args['groupdict'] = match.groupdict()
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.CallbackQueryHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")

View file

@ -21,7 +21,6 @@ import warnings
from .handler import Handler
from telegram import Update
from telegram.utils.deprecate import deprecate
class CommandHandler(Handler):
@ -110,7 +109,7 @@ class CommandHandler(Handler):
res = self.filters(message)
return res and (message.text.startswith('/') and command[0] == self.command
and command[1] == update.message.bot.username)
and command[1].lower() == update.message.bot.username.lower())
else:
return False
@ -126,8 +125,3 @@ class CommandHandler(Handler):
optional_args['args'] = message.text.split()[1:]
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.CommandHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")

View file

@ -145,7 +145,9 @@ class ConversationHandler(Handler):
raise ValueError("If 'per_chat=True', 'InlineQueryHandler' doesn't work")
def _get_key(self, update):
chat, user = update.extract_chat_and_user()
chat = update.effective_chat
user = update.effective_user
key = list()
if self.per_chat:
@ -181,6 +183,7 @@ class ConversationHandler(Handler):
res = new_state.result(timeout=self.run_async_timeout)
except Exception as exc:
self.logger.exception("Promise function raised exception")
self.logger.exception("{}".format(exc))
error = True
if not error and new_state.done.is_set():

View file

@ -32,7 +32,6 @@ from future.builtins import range
from telegram import TelegramError
from telegram.ext.handler import Handler
from telegram.utils.deprecate import deprecate
from telegram.utils.promise import Promise
logging.getLogger(__name__).addHandler(logging.NullHandler())
@ -368,11 +367,3 @@ class Dispatcher(object):
for callback in self.error_handlers:
callback(self.bot, update, error)
# old non-PEP8 Dispatcher methods
m = "telegram.dispatcher."
addHandler = deprecate(add_handler, m + "AddHandler", m + "add_handler")
removeHandler = deprecate(remove_handler, m + "removeHandler", m + "remove_handler")
addErrorHandler = deprecate(add_error_handler, m + "addErrorHandler", m + "add_error_handler")
removeErrorHandler = deprecate(remove_error_handler, m + "removeErrorHandler",
m + "remove_error_handler")

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
""" This module contains the Filters for use with the MessageHandler class """
from telegram import Chat
class BaseFilter(object):
@ -32,9 +33,14 @@ class BaseFilter(object):
>>> (Filters.audio | Filters.video)
Not:
>>> ~ Filters.command
Also works with more than two filters:
>>> (Filters.text & (Filters.entity(URL) | Filters.entity(TEXT_LINK)))
>>> Filters.text & (~ Filters.forwarded)
If you want to create your own filters create a class inheriting from this class and implement
a `filter` method that returns a boolean: `True` if the message should be handled, `False`
@ -51,10 +57,32 @@ class BaseFilter(object):
def __or__(self, other):
return MergedFilter(self, or_filter=other)
def __invert__(self):
return InvertedFilter(self)
def filter(self, message):
raise NotImplementedError
class InvertedFilter(BaseFilter):
"""Represents a filter that has been inverted.
Args:
f: The filter to invert
"""
def __init__(self, f):
self.f = f
def filter(self, message):
return not self.f(message)
def __str__(self):
return "<telegram.ext.filters.InvertedFilter inverting {}>".format(self.f)
__repr__ = __str__
class MergedFilter(BaseFilter):
"""Represents a filter consisting of two other filters.
@ -221,3 +249,17 @@ class Filters(object):
def filter(self, message):
return any([entity.type == self.entity_type for entity in message.entities])
class _Private(BaseFilter):
def filter(self, message):
return message.chat.type == Chat.PRIVATE
private = _Private()
class _Group(BaseFilter):
def filter(self, message):
return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP]
group = _Group()

View file

@ -19,8 +19,6 @@
""" This module contains the base class for handlers as used by the
Dispatcher """
from telegram.utils.deprecate import deprecate
class Handler(object):
"""
@ -104,19 +102,13 @@ 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 = update.extract_chat_and_user()
chat = update.effective_chat
user = update.effective_user
if self.pass_user_data:
optional_args['user_data'] = dispatcher.user_data[user.id]
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
if self.pass_chat_data:
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
return optional_args
# old non-PEP8 Handler methods
m = "telegram.Handler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")
collectOptionalArgs = deprecate(collect_optional_args, m + "collectOptionalArgs",
m + "collect_optional_args")

View file

@ -21,7 +21,6 @@ import warnings
from .handler import Handler
from telegram import Update
from telegram.utils.deprecate import deprecate
class MessageHandler(Handler):
@ -53,7 +52,7 @@ class MessageHandler(Handler):
For each update in the same chat, it will be the same ``dict``. Default is ``False``.
message_updates (Optional[bool]): Should "normal" message updates be handled? Default is
``True``.
channel_posts_updates (Optional[bool]): Should channel posts updates be handled? Default is
channel_post_updates (Optional[bool]): Should channel posts updates be handled? Default is
``True``.
"""
@ -67,8 +66,8 @@ class MessageHandler(Handler):
pass_user_data=False,
pass_chat_data=False,
message_updates=True,
channel_posts_updates=True):
if not message_updates and not channel_posts_updates:
channel_post_updates=True):
if not message_updates and not channel_post_updates:
raise ValueError('Both message_updates & channel_post_updates are False')
super(MessageHandler, self).__init__(
@ -80,7 +79,7 @@ class MessageHandler(Handler):
self.filters = filters
self.allow_edited = allow_edited
self.message_updates = message_updates
self.channel_posts_updates = channel_posts_updates
self.channel_post_updates = channel_post_updates
# We put this up here instead of with the rest of checking code
# in check_update since we don't wanna spam a ton
@ -94,7 +93,7 @@ class MessageHandler(Handler):
and (update.message or (update.edited_message and self.allow_edited)))
def _is_allowed_channel_post(self, update):
return (self.channel_posts_updates
return (self.channel_post_updates
and (update.channel_post or (update.edited_channel_post and self.allow_edited)))
def check_update(self, update):
@ -105,8 +104,7 @@ class MessageHandler(Handler):
res = True
else:
message = (update.message or update.edited_message or update.channel_post
or update.edited_channel_post)
message = update.effective_message
if isinstance(self.filters, list):
res = any(func(message) for func in self.filters)
else:
@ -121,8 +119,3 @@ class MessageHandler(Handler):
optional_args = self.collect_optional_args(dispatcher, update)
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.MessageHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")

View file

@ -0,0 +1,330 @@
#!/usr/bin/env python
#
# Module author:
# Tymofii A. Khodniev (thodnev) <thodnev@mail.ru>
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# 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/]
'''A throughput-limiting message processor for Telegram bots'''
from telegram.utils import promise
import functools
import sys
import time
import threading
if sys.version_info.major > 2:
import queue as q
else:
import Queue as q
# We need to count < 1s intervals, so the most accurate timer is needed
# Starting from Python 3.3 we have time.perf_counter which is the clock
# with the highest resolution available to the system, so let's use it there.
# In Python 2.7, there's no perf_counter yet, so fallback on what we have:
# on Windows, the best available is time.clock while time.time is on
# another platforms (M. Lutz, "Learning Python," 4ed, p.630-634)
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
curtime = time.perf_counter # pylint: disable=E1101
else:
curtime = time.clock if sys.platform[:3] == 'win' else time.time
class DelayQueueError(RuntimeError):
'''Indicates processing errors'''
pass
class DelayQueue(threading.Thread):
'''Processes callbacks from queue with specified throughput limits.
Creates a separate thread to process callbacks with delays.
Args:
queue (:obj:`queue.Queue`, optional): used to pass callbacks to
thread.
Creates `queue.Queue` implicitly if not provided.
burst_limit (:obj:`int`, optional): number of maximum callbacks to
process per time-window defined by `time_limit_ms`.
Defaults to 30.
time_limit_ms (:obj:`int`, optional): defines width of time-window
used when each processing limit is calculated.
Defaults to 1000.
exc_route (:obj:`callable`, optional): a callable, accepting 1
positional argument; used to route exceptions from processor
thread to main thread; is called on `Exception` subclass
exceptions.
If not provided, exceptions are routed through dummy handler,
which re-raises them.
autostart (:obj:`bool`, optional): if True, processor is started
immediately after object's creation; if False, should be
started manually by `start` method.
Defaults to True.
name (:obj:`str`, optional): thread's name.
Defaults to ``'DelayQueue-N'``, where N is sequential
number of object created.
'''
_instcnt = 0 # instance counter
def __init__(self,
queue=None,
burst_limit=30,
time_limit_ms=1000,
exc_route=None,
autostart=True,
name=None):
self._queue = queue if queue is not None else q.Queue()
self.burst_limit = burst_limit
self.time_limit = time_limit_ms / 1000
self.exc_route = (exc_route if exc_route is not None else self._default_exception_handler)
self.__exit_req = False # flag to gently exit thread
self.__class__._instcnt += 1
if name is None:
name = '%s-%s' % (self.__class__.__name__, self.__class__._instcnt)
super(DelayQueue, self).__init__(name=name)
self.daemon = False
if autostart: # immediately start processing
super(DelayQueue, self).start()
def run(self):
'''Do not use the method except for unthreaded testing purposes,
the method normally is automatically called by `start` method.
'''
times = [] # used to store each callable processing time
while True:
item = self._queue.get()
if self.__exit_req:
return # shutdown thread
# delay routine
now = curtime()
t_delta = now - self.time_limit # calculate early to improve perf.
if times and t_delta > times[-1]:
# if last call was before the limit time-window
# used to impr. perf. in long-interval calls case
times = [now]
else:
# collect last in current limit time-window
times = [t for t in times if t >= t_delta]
times.append(now)
if len(times) >= self.burst_limit: # if throughput limit was hit
time.sleep(times[1] - t_delta)
# finally process one
try:
func, args, kwargs = item
func(*args, **kwargs)
except Exception as exc: # re-route any exceptions
self.exc_route(exc) # to prevent thread exit
def stop(self, timeout=None):
'''Used to gently stop processor and shutdown its thread.
Args:
timeout (:obj:`float`): indicates maximum time to wait for
processor to stop and its thread to exit.
If timeout exceeds and processor has not stopped, method
silently returns. `is_alive` could be used afterwards
to check the actual status. If `timeout` set to None, blocks
until processor is shut down.
Defaults to None.
Returns:
None
'''
self.__exit_req = True # gently request
self._queue.put(None) # put something to unfreeze if frozen
super(DelayQueue, self).join(timeout=timeout)
@staticmethod
def _default_exception_handler(exc):
'''Dummy exception handler which re-raises exception in thread.
Could be possibly overwritten by subclasses.
'''
raise exc
def __call__(self, func, *args, **kwargs):
'''Used to process callbacks in throughput-limiting thread
through queue.
Args:
func (:obj:`callable`): the actual function (or any callable) that
is processed through queue.
*args: variable-length `func` arguments.
**kwargs: arbitrary keyword-arguments to `func`.
Returns:
None
'''
if not self.is_alive() or self.__exit_req:
raise DelayQueueError('Could not process callback in stopped thread')
self._queue.put((func, args, kwargs))
# The most straightforward way to implement this is to use 2 sequenital delay
# queues, like on classic delay chain schematics in electronics.
# So, message path is:
# msg --> group delay if group msg, else no delay --> normal msg delay --> out
# This way OS threading scheduler cares of timings accuracy.
# (see time.time, time.clock, time.perf_counter, time.sleep @ docs.python.org)
class MessageQueue(object):
'''Implements callback processing with proper delays to avoid hitting
Telegram's message limits.
Contains two `DelayQueue`s, for group and for all messages, interconnected
in delay chain. Callables are processed through *group* `DelayQueue`, then
through *all* `DelayQueue` for group-type messages. For non-group messages,
only the *all* `DelayQueue` is used.
Args:
all_burst_limit (:obj:`int`, optional): numer of maximum *all-type*
callbacks to process per time-window defined by
`all_time_limit_ms`.
Defaults to 30.
all_time_limit_ms (:obj:`int`, optional): defines width of *all-type*
time-window used when each processing limit is calculated.
Defaults to 1000 ms.
group_burst_limit (:obj:`int`, optional): numer of maximum *group-type*
callbacks to process per time-window defined by
`group_time_limit_ms`.
Defaults to 20.
group_time_limit_ms (:obj:`int`, optional): defines width of
*group-type* time-window used when each processing limit is
calculated.
Defaults to 60000 ms.
exc_route (:obj:`callable`, optional): a callable, accepting one
positional argument; used to route exceptions from processor
threads to main thread; is called on `Exception` subclass
exceptions.
If not provided, exceptions are routed through dummy handler,
which re-raises them.
autostart (:obj:`bool`, optional): if True, processors are started
immediately after object's creation; if False, should be
started manually by `start` method.
Defaults to True.
Attributes:
_all_delayq (:obj:`telegram.ext.messagequeue.DelayQueue`): actual
`DelayQueue` used for *all-type* callback processing
_group_delayq (:obj:`telegram.ext.messagequeue.DelayQueue`): actual
`DelayQueue` used for *group-type* callback processing
'''
def __init__(self,
all_burst_limit=30,
all_time_limit_ms=1000,
group_burst_limit=20,
group_time_limit_ms=60000,
exc_route=None,
autostart=True):
# create accoring delay queues, use composition
self._all_delayq = DelayQueue(
burst_limit=all_burst_limit,
time_limit_ms=all_time_limit_ms,
exc_route=exc_route,
autostart=autostart)
self._group_delayq = DelayQueue(
burst_limit=group_burst_limit,
time_limit_ms=group_time_limit_ms,
exc_route=exc_route,
autostart=autostart)
def start(self):
'''Method is used to manually start the `MessageQueue` processing
Returns:
None
'''
self._all_delayq.start()
self._group_delayq.start()
def stop(self, timeout=None):
self._group_delayq.stop(timeout=timeout)
self._all_delayq.stop(timeout=timeout)
stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docsting if any
def __call__(self, promise, is_group_msg=False):
'''Processes callables in troughput-limiting queues to avoid
hitting limits (specified with \*_burst_limit and *\_time_limit_ms).
Args:
promise (:obj:`callable`): mainly the
:obj:`telegram.utils.promise.Promise` (see Notes for other
callables), that is processed in delay queues
is_group_msg (:obj:`bool`, optional): defines whether `promise`
would be processed in *group*+*all* `DelayQueue`s
(if set to ``True``), or only through *all* `DelayQueue`
(if set to ``False``), resulting in needed delays to avoid
hitting specified limits.
Defaults to ``True``.
Notes:
Method is designed to accept :obj:`telegram.utils.promise.Promise`
as `promise` argument, but other callables could be used too.
For example, lambdas or simple functions could be used to wrap
original func to be called with needed args.
In that case, be sure that either wrapper func does not raise
outside exceptions or the proper `exc_route` handler is provided.
Returns:
:obj:`callable` used as `promise` argument.
'''
if not is_group_msg: # ignore middle group delay
self._all_delayq(promise)
else: # use middle group delay
self._group_delayq(self._all_delayq, promise)
return promise
def queuedmessage(method):
'''A decorator to be used with `telegram.bot.Bot` send* methods.
Note:
As it probably wouldn't be a good idea to make this decorator a
property, it had been coded as decorator function, so it implies that
**first positional argument to wrapped MUST be self**\.
The next object attributes are used by decorator:
Attributes:
self._is_messages_queued_default (:obj:`bool`): Value to provide
class-defaults to `queued` kwarg if not provided during wrapped
method call.
self._msg_queue (:obj:`telegram.ext.messagequeue.MessageQueue`):
The actual `MessageQueue` used to delay outbond messages according
to specified time-limits.
Wrapped method starts accepting the next kwargs:
Args:
queued (:obj:`bool`, optional): if set to ``True``, the `MessageQueue`
is used to process output messages.
Defaults to `self._is_queued_out`.
isgroup (:obj:`bool`, optional): if set to ``True``, the message is
meant to be group-type (as there's no obvious way to determine its
type in other way at the moment). Group-type messages could have
additional processing delay according to limits set in
`self._out_queue`.
Defaults to ``False``.
Returns:
Either :obj:`telegram.utils.promise.Promise` in case call is queued,
or original method's return value if it's not.
'''
@functools.wraps(method)
def wrapped(self, *args, **kwargs):
queued = kwargs.pop('queued', self._is_messages_queued_default)
isgroup = kwargs.pop('isgroup', False)
if queued:
prom = promise.Promise(method, args, kwargs)
return self._msg_queue(prom, isgroup)
return method(self, *args, **kwargs)
return wrapped

View file

@ -71,7 +71,10 @@ class RegexHandler(Handler):
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
pass_chat_data=False,
allow_edited=False,
message_updates=True,
channel_post_updates=False):
super(RegexHandler, self).__init__(
callback,
pass_update_queue=pass_update_queue,
@ -85,17 +88,30 @@ class RegexHandler(Handler):
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
self.allow_edited = allow_edited
self.message_updates = message_updates
self.channel_post_updates = channel_post_updates
def _is_allowed_message(self, update):
return (self.message_updates
and (update.message or (update.edited_message and self.allow_edited)))
def _is_allowed_channel_post(self, update):
return (self.channel_post_updates
and (update.channel_post or (update.edited_channel_post and self.allow_edited)))
def check_update(self, update):
if isinstance(update, Update) and update.message and update.message.text:
match = re.match(self.pattern, update.message.text)
if (isinstance(update, Update)
and (self._is_allowed_message(update) or self._is_allowed_channel_post(update))
and update.effective_message.text):
match = re.match(self.pattern, update.effective_message.text)
return bool(match)
else:
return False
def handle_update(self, update, dispatcher):
optional_args = self.collect_optional_args(dispatcher, update)
match = re.match(self.pattern, update.message.text)
match = re.match(self.pattern, update.effective_message.text)
if self.pass_groups:
optional_args['groups'] = match.groups()

View file

@ -19,7 +19,6 @@
""" This module contains the StringCommandHandler class """
from .handler import Handler
from telegram.utils.deprecate import deprecate
class StringCommandHandler(Handler):
@ -69,8 +68,3 @@ class StringCommandHandler(Handler):
optional_args['args'] = update.split()[1:]
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.StringCommandHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")

View file

@ -23,7 +23,6 @@ import re
from future.utils import string_types
from .handler import Handler
from telegram.utils.deprecate import deprecate
class StringRegexHandler(Handler):
@ -84,8 +83,3 @@ class StringRegexHandler(Handler):
optional_args['groupdict'] = match.groupdict()
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.StringRegexHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")

View file

@ -62,6 +62,9 @@ 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.
user_sig_handler (Optional[function]): Takes ``signum, frame`` as positional arguments.
This will be called when a signal is received, defaults are (SIGINT, SIGTERM, SIGABRT)
setable with Updater.idle(stop_signals=(signals))
request_kwargs (Optional[dict]): Keyword args to control the creation of a request object
(ignored if `bot` argument is used).
@ -71,7 +74,14 @@ class Updater(object):
"""
_request = None
def __init__(self, token=None, base_url=None, workers=4, bot=None, request_kwargs=None):
def __init__(self,
token=None,
base_url=None,
workers=4,
bot=None,
user_sig_handler=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):
@ -92,6 +102,7 @@ class Updater(object):
request_kwargs['con_pool_size'] = workers + 4
self._request = Request(**request_kwargs)
self.bot = Bot(token, base_url, request=self._request)
self.user_sig_handler = user_sig_handler
self.update_queue = Queue()
self.job_queue = JobQueue(self.bot)
self.__exception_event = Event()
@ -192,7 +203,8 @@ class Updater(object):
key=None,
clean=False,
bootstrap_retries=0,
webhook_url=None):
webhook_url=None,
allowed_updates=None):
"""
Starts a small http server to listen for updates via webhook. If cert
and key are not provided, the webhook will be started directly on
@ -215,9 +227,10 @@ class Updater(object):
| < 0 - retry indefinitely
| 0 - no retries (default)
| > 0 - retry up to X times
webhook_url (Optional[str]): Explicitly specifiy the webhook url.
webhook_url (Optional[str]): Explicitly specify the webhook url.
Useful behind NAT, reverse proxy, etc. Default is derived from
`listen`, `port` & `url_path`.
allowed_updates (Optional[list[str]]): Passed to Bot.setWebhook
Returns:
Queue: The update queue that can be filled from the main thread
@ -231,7 +244,7 @@ class Updater(object):
self.job_queue.start()
self._init_thread(self.dispatcher.start, "dispatcher"),
self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert,
key, bootstrap_retries, clean, webhook_url)
key, bootstrap_retries, clean, webhook_url, allowed_updates)
# Return the update queue so the main thread can insert updates
return self.update_queue
@ -247,7 +260,7 @@ class Updater(object):
cur_interval = poll_interval
self.logger.debug('Updater thread started')
self._bootstrap(bootstrap_retries, clean=clean, webhook_url='')
self._bootstrap(bootstrap_retries, clean=clean, webhook_url='', allowed_updates=None)
while self.running:
try:
@ -295,7 +308,7 @@ class Updater(object):
return current_interval
def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean,
webhook_url):
webhook_url, allowed_updates):
self.logger.debug('Updater thread started')
use_ssl = cert is not None and key is not None
if not url_path.startswith('/'):
@ -316,7 +329,8 @@ class Updater(object):
max_retries=bootstrap_retries,
clean=clean,
webhook_url=webhook_url,
cert=open(cert, 'rb'))
cert=open(cert, 'rb'),
allowed_updates=allowed_updates)
elif clean:
self.logger.warning("cleaning updates is not supported if "
"SSL-termination happens elsewhere; skipping")
@ -346,7 +360,7 @@ class Updater(object):
def _gen_webhook_url(listen, port, url_path):
return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path)
def _bootstrap(self, max_retries, clean, webhook_url, cert=None):
def _bootstrap(self, max_retries, clean, webhook_url, allowed_updates, cert=None):
retries = 0
while 1:
@ -357,7 +371,8 @@ class Updater(object):
self._clean_updates()
sleep(1)
self.bot.setWebhook(url=webhook_url, certificate=cert)
self.bot.setWebhook(
url=webhook_url, certificate=cert, allowed_updates=allowed_updates)
except (Unauthorized, InvalidToken):
raise
except TelegramError:
@ -422,6 +437,8 @@ class Updater(object):
self.is_idle = False
if self.running:
self.stop()
if self.user_sig_handler:
self.user_sig_handler(signum, frame)
else:
self.logger.warning('Exiting immediately!')
import os

View file

@ -202,7 +202,7 @@ class Message(TelegramObject):
data = super(Message, Message).de_json(data, bot)
data['from_user'] = User.de_json(data.get('from'), bot)
data['date'] = datetime.fromtimestamp(data['date'])
data['date'] = Message._fromtimestamp(data['date'])
data['chat'] = Chat.de_json(data.get('chat'), bot)
data['entities'] = MessageEntity.de_list(data.get('entities'), bot)
data['forward_from'] = User.de_json(data.get('forward_from'), bot)

View file

@ -73,6 +73,10 @@ class Update(TelegramObject):
self.channel_post = channel_post
self.edited_channel_post = edited_channel_post
self._effective_user = None
self._effective_chat = None
self._effective_message = None
@staticmethod
def de_json(data, bot):
"""
@ -99,25 +103,23 @@ class Update(TelegramObject):
return Update(**data)
def extract_chat_and_user(self):
@property
def effective_user(self):
"""
A property that contains the ``User`` that sent this update, no matter what kind of update
this is. Will be ``None`` for channel posts.
"""
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.
"""
if self._effective_user:
return self._effective_user
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
@ -127,51 +129,67 @@ class Update(TelegramObject):
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
self._effective_user = user
return user
def extract_message_text(self):
@property
def effective_chat(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
A property that contains the ``Chat`` that this update was sent in, no matter what kind of
update this is. Will be ``None`` for inline queries and chosen inline results.
"""
if self._effective_chat:
return self._effective_chat
chat = None
if self.message:
return self.message.text
chat = self.message.chat
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.")
chat = self.edited_message.chat
def extract_entities(self):
elif self.callback_query and self.callback_query.message:
chat = self.callback_query.message.chat
elif self.channel_post:
chat = self.channel_post.chat
elif self.edited_channel_post:
chat = self.edited_channel_post.chat
self._effective_chat = chat
return chat
@property
def effective_message(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
A property that contains the ``Message`` included in this update, no matter what kind
of update this is. Will be ``None`` for inline queries, chosen inline results and callback
queries from inline messages.
"""
if self._effective_message:
return self._effective_message
message = None
if self.message:
return self.message.parse_entities()
message = self.message
elif self.edited_message:
return self.edited_message.parse_entities()
message = self.edited_message
elif self.callback_query:
return self.callback_query.message.parse_entities()
else:
raise ValueError("No message object found in self, therefore no entities available.")
message = self.callback_query.message
elif self.channel_post:
message = self.channel_post
elif self.edited_channel_post:
message = self.edited_channel_post
self._effective_message = message
return message

View file

@ -28,6 +28,7 @@ except ImportError:
import certifi
import urllib3
import urllib3.contrib.appengine
from urllib3.connection import HTTPConnection
from urllib3.util.timeout import Timeout
try:
@ -93,7 +94,11 @@ class Request(object):
proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
if not proxy_url:
mgr = urllib3.PoolManager(**kwargs)
if urllib3.contrib.appengine.is_appengine_sandbox():
# Use URLFetch service if running in App Engine
mgr = urllib3.contrib.appengine.AppEngineManager()
else:
mgr = urllib3.PoolManager(**kwargs)
else:
kwargs.update(urllib3_proxy_kwargs)
if proxy_url.startswith('socks'):

View file

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

View file

@ -178,6 +178,18 @@ class FiltersTest(BaseTest, unittest.TestCase):
self.message.entities = [self.e(MessageEntity.BOLD), self.e(MessageEntity.MENTION)]
self.assertTrue(Filters.entity(MessageEntity.MENTION)(self.message))
def test_private_filter(self):
self.assertTrue(Filters.private(self.message))
self.message.chat.type = "group"
self.assertFalse(Filters.private(self.message))
def test_group_fileter(self):
self.assertFalse(Filters.group(self.message))
self.message.chat.type = "group"
self.assertTrue(Filters.group(self.message))
self.message.chat.type = "supergroup"
self.assertTrue(Filters.group(self.message))
def test_and_filters(self):
self.message.text = 'test'
self.message.forward_date = True
@ -226,6 +238,32 @@ class FiltersTest(BaseTest, unittest.TestCase):
r"<telegram.ext.filters.(Filters.)?_Forwarded object at .*?> or "
r"<telegram.ext.filters.(Filters.)?entity object at .*?>>>")
def test_inverted_filters(self):
self.message.text = '/test'
self.assertTrue((Filters.command)(self.message))
self.assertFalse((~Filters.command)(self.message))
self.message.text = 'test'
self.assertFalse((Filters.command)(self.message))
self.assertTrue((~Filters.command)(self.message))
def test_inverted_and_filters(self):
self.message.text = '/test'
self.message.forward_date = 1
self.assertTrue((Filters.forwarded & Filters.command)(self.message))
self.assertFalse((~Filters.forwarded & Filters.command)(self.message))
self.assertFalse((Filters.forwarded & ~Filters.command)(self.message))
self.assertFalse((~(Filters.forwarded & Filters.command))(self.message))
self.message.forward_date = None
self.assertFalse((Filters.forwarded & Filters.command)(self.message))
self.assertTrue((~Filters.forwarded & Filters.command)(self.message))
self.assertFalse((Filters.forwarded & ~Filters.command)(self.message))
self.assertTrue((~(Filters.forwarded & Filters.command))(self.message))
self.message.text = 'test'
self.assertFalse((Filters.forwarded & Filters.command)(self.message))
self.assertFalse((~Filters.forwarded & Filters.command)(self.message))
self.assertFalse((Filters.forwarded & ~Filters.command)(self.message))
self.assertTrue((~(Filters.forwarded & Filters.command))(self.message))
def test_faulty_custom_filter(self):
class _CustomFilter(BaseFilter):

View file

@ -0,0 +1,91 @@
'''This module contains telegram.ext.messagequeue test logic'''
from __future__ import print_function, division
import sys
import os
import time
import unittest
sys.path.insert(0, os.path.dirname(__file__) + os.sep + '..')
from tests.base import BaseTest
from telegram.ext import messagequeue as mq
class DelayQueueTest(BaseTest, unittest.TestCase):
def __init__(self, *args, **kwargs):
self._N = kwargs.pop('N', 128)
self._msglimit = kwargs.pop('burst_limit', 30)
self._timelimit = kwargs.pop('time_limit_ms', 1000)
self._margin = kwargs.pop('margin_ms', 0)
isprint = kwargs.pop('isprint', False)
def noprint(*args, **kwargs):
pass
self._print = print if isprint else noprint
super(DelayQueueTest, self).__init__(*args, **kwargs)
def setUp(self):
print = self._print
print('Self-test with N = {} msgs, burst_limit = {} msgs, '
'time_limit = {:.2f} ms, margin = {:.2f} ms'
''.format(self._N, self._msglimit, self._timelimit, self._margin))
self.testtimes = []
def testcall():
self.testtimes.append(mq.curtime())
self.testcall = testcall
def test_delayqueue_limits(self):
'''Test that DelayQueue dispatched calls don't hit time-window limit'''
print = self._print
dsp = mq.DelayQueue(
burst_limit=self._msglimit, time_limit_ms=self._timelimit, autostart=True)
print('Started dispatcher {}\nStatus: {}'
''.format(dsp, ['inactive', 'active'][dsp.is_alive()]))
self.assertTrue(dsp.is_alive())
print('Dispatching {} calls @ {}'.format(self._N, time.asctime()))
for i in range(self._N):
dsp(self.testcall)
print('Queue filled, waiting 4 dispatch finish @ ' + str(time.asctime()))
starttime = mq.curtime()
app_endtime = (
(self._N * self._msglimit /
(1000 * self._timelimit)) + starttime + 20) # wait up to 20 sec more than needed
while not dsp._queue.empty() and mq.curtime() < app_endtime:
time.sleep(1)
self.assertTrue(dsp._queue.empty()) # check loop exit condition
dsp.stop()
print('Dispatcher ' + ['stopped', '!NOT STOPPED!'][dsp.is_alive()] + ' @ ' + str(
time.asctime()))
self.assertFalse(dsp.is_alive())
self.assertTrue(self.testtimes or self._N == 0)
print('Calculating call time windows')
passes, fails = [], []
delta = (self._timelimit - self._margin) / 1000
it = enumerate(range(self._msglimit + 1, len(self.testtimes)))
for start, stop in it:
part = self.testtimes[start:stop]
if (part[-1] - part[0]) >= delta:
passes.append(part)
else:
fails.append(part)
print('Tested: {}, Passed: {}, Failed: {}'
''.format(len(passes + fails), len(passes), len(fails)))
if fails:
print('(!) Got mismatches: ' + ';\n'.join(map(str, fails)))
self.assertFalse(fails)
if __name__ == '__main__':
unittest.main()

View file

@ -76,16 +76,20 @@ 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):
def test_effective_chat(self):
update = telegram.Update.de_json(self.json_dict, self._bot)
chat, user = update.extract_chat_and_user()
chat = update.effective_chat
self.assertEqual(update.message.chat, chat)
def test_effective_user(self):
update = telegram.Update.de_json(self.json_dict, self._bot)
user = update.effective_user
self.assertEqual(update.message.from_user, user)
def test_extract_message_text(self):
def test_effective_message(self):
update = telegram.Update.de_json(self.json_dict, self._bot)
text = update.extract_message_text()
self.assertEqual(update.message.text, text)
message = update.effective_message
self.assertEqual(update.message.text, message.text)
if __name__ == '__main__':

View file

@ -189,15 +189,15 @@ class UpdaterTest(BaseTest, unittest.TestCase):
d = self.updater.dispatcher
from telegram.ext import Filters
handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, allow_edited=True)
d.addHandler(handler)
d.add_handler(handler)
self.updater.start_polling(0.01)
sleep(.1)
self.assertEqual(self.received_message, 'Test')
# Remove handler
d.removeHandler(handler)
d.remove_handler(handler)
handler = MessageHandler(Filters.text, self.telegramHandlerEditedTest, allow_edited=False)
d.addHandler(handler)
d.add_handler(handler)
self.reset()
self.updater.bot.send_messages = 1
@ -250,6 +250,10 @@ class UpdaterTest(BaseTest, unittest.TestCase):
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertEqual(self.received_message, '/test@MockBot')
message.text = "/test@mockbot"
queue.put(Update(update_id=0, message=message))
sleep(.1)
self.assertEqual(self.received_message, '/test@mockbot')
# directed at other bot
self.reset()
@ -259,9 +263,9 @@ class UpdaterTest(BaseTest, unittest.TestCase):
self.assertTrue(None is self.received_message)
# Remove handler
d.removeHandler(handler)
d.remove_handler(handler)
handler = CommandHandler('test', self.telegramHandlerEditedTest, allow_edited=False)
d.addHandler(handler)
d.add_handler(handler)
self.reset()
self.updater.bot.send_messages = 1
@ -716,7 +720,8 @@ class UpdaterTest(BaseTest, unittest.TestCase):
ip = '127.0.0.1'
port = randrange(1024, 49152) # select random port for travis
thr = Thread(
target=self.updater._start_webhook, args=(ip, port, '', None, None, 0, False, None))
target=self.updater._start_webhook,
args=(ip, port, '', None, None, 0, False, None, None))
thr.start()
sleep(0.5)
@ -789,6 +794,23 @@ class UpdaterTest(BaseTest, unittest.TestCase):
sleep(1)
self.assertFalse(self.updater.running)
def test_userSignal(self):
self._setup_updater('Test7', messages=0)
tempVar = {'a': 0}
def userSignalInc(signum, frame):
tempVar['a'] = 1
self.updater.user_sig_handler = userSignalInc
self.updater.start_polling(poll_interval=0.01)
Thread(target=self.signalsender).start()
self.updater.idle()
# If we get this far, idle() ran through
sleep(1)
self.assertFalse(self.updater.running)
self.assertTrue(tempVar['a'] != 0)
def test_createBot(self):
self.updater = Updater('123:abcd')
self.assertIsNotNone(self.updater.bot)
@ -832,7 +854,7 @@ class MockBot(object):
return update
def setWebhook(self, url=None, certificate=None):
def setWebhook(self, url=None, certificate=None, allowed_updates=None):
if self.bootstrap_retries is None:
return