import sys import time import json import threading import traceback import collections try: import Queue as queue except ImportError: import queue from . import exception from . import _find_first_key, flavor_router class RunForeverAsThread(object): def run_as_thread(self, *args, **kwargs): t = threading.Thread(target=self.run_forever, args=args, kwargs=kwargs) t.daemon = True t.start() class CollectLoop(RunForeverAsThread): def __init__(self, handle): self._handle = handle self._inqueue = queue.Queue() @property def input_queue(self): return self._inqueue def run_forever(self): while 1: try: msg = self._inqueue.get(block=True) self._handle(msg) except: traceback.print_exc() class GetUpdatesLoop(RunForeverAsThread): def __init__(self, bot, on_update): self._bot = bot self._update_handler = on_update def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None): """ Process new updates in infinity loop :param relax: float :param offset: int :param timeout: int :param allowed_updates: bool """ while 1: try: result = self._bot.getUpdates(offset=offset, timeout=timeout, allowed_updates=allowed_updates) # Once passed, this parameter is no longer needed. allowed_updates = None # No sort. Trust server to give messages in correct order. for update in result: self._update_handler(update) offset = update['update_id'] + 1 except exception.BadHTTPResponse as e: traceback.print_exc() # Servers probably down. Wait longer. if e.status == 502: time.sleep(30) except: traceback.print_exc() finally: time.sleep(relax) def _dictify3(data): if type(data) is bytes: return json.loads(data.decode('utf-8')) elif type(data) is str: return json.loads(data) elif type(data) is dict: return data else: raise ValueError() def _dictify27(data): if type(data) in [str, unicode]: return json.loads(data) elif type(data) is dict: return data else: raise ValueError() _dictify = _dictify3 if sys.version_info >= (3,) else _dictify27 def _extract_message(update): key = _find_first_key(update, ['message', 'edited_message', 'channel_post', 'edited_channel_post', 'callback_query', 'inline_query', 'chosen_inline_result', 'shipping_query', 'pre_checkout_query', 'my_chat_member']) return key, update[key] def _infer_handler_function(bot, h): if h is None: return bot.handle elif isinstance(h, dict): return flavor_router(h) else: return h class MessageLoop(RunForeverAsThread): def __init__(self, bot, handle=None): self._bot = bot self._handle = _infer_handler_function(bot, handle) def run_forever(self, *args, **kwargs): """ :type relax: float :param relax: seconds between each :meth:`.getUpdates` :type offset: int :param offset: initial ``offset`` parameter supplied to :meth:`.getUpdates` :type timeout: int :param timeout: ``timeout`` parameter supplied to :meth:`.getUpdates`, controlling how long to poll. :type allowed_updates: array of string :param allowed_updates: ``allowed_updates`` parameter supplied to :meth:`.getUpdates`, controlling which types of updates to receive. Calling this method will block forever. Use :meth:`.run_as_thread` to run it non-blockingly. """ collectloop = CollectLoop(self._handle) updatesloop = GetUpdatesLoop(self._bot, lambda update: collectloop.input_queue.put(_extract_message(update)[1])) # feed messages to collect loop # feed events to collect loop self._bot.scheduler.on_event(collectloop.input_queue.put) self._bot.scheduler.run_as_thread() updatesloop.run_as_thread(*args, **kwargs) collectloop.run_forever() # blocking class Webhook(RunForeverAsThread): def __init__(self, bot, handle=None): self._bot = bot self._collectloop = CollectLoop(_infer_handler_function(bot, handle)) def run_forever(self): # feed events to collect loop self._bot.scheduler.on_event(self._collectloop.input_queue.put) self._bot.scheduler.run_as_thread() self._collectloop.run_forever() def feed(self, data): update = _dictify(data) self._collectloop.input_queue.put(_extract_message(update)[1]) class Orderer(RunForeverAsThread): def __init__(self, on_ordered_update): self._on_ordered_update = on_ordered_update self._inqueue = queue.Queue() @property def input_queue(self): return self._inqueue def run_forever(self, maxhold=3): def handle(update): self._on_ordered_update(update) return update['update_id'] # Here is the re-ordering mechanism, ensuring in-order delivery of updates. max_id = None # max update_id passed to callback buffer = collections.deque() # keep those updates which skip some update_id qwait = None # how long to wait for updates, # because buffer's content has to be returned in time. while 1: try: update = self._inqueue.get(block=True, timeout=qwait) if max_id is None: # First message received, handle regardless. max_id = handle(update) elif update['update_id'] == max_id + 1: # No update_id skipped, handle naturally. max_id = handle(update) # clear contagious updates in buffer if len(buffer) > 0: buffer.popleft() # first element belongs to update just received, useless now. while 1: try: if type(buffer[0]) is dict: max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them. else: break # gap, no more contagious updates except IndexError: break # buffer empty elif update['update_id'] > max_id + 1: # Update arrives pre-maturely, insert to buffer. nbuf = len(buffer) if update['update_id'] <= max_id + nbuf: # buffer long enough, put update at position buffer[update['update_id'] - max_id - 1] = update else: # buffer too short, lengthen it expire = time.time() + maxhold for a in range(nbuf, update['update_id']-max_id-1): buffer.append(expire) # put expiry time in gaps buffer.append(update) else: pass # discard except queue.Empty: # debug message # print('Timeout') # some buffer contents have to be handled # flush buffer until a non-expired time is encountered while 1: try: if type(buffer[0]) is dict: max_id = handle(buffer.popleft()) else: expire = buffer[0] if expire <= time.time(): max_id += 1 buffer.popleft() else: break # non-expired except IndexError: break # buffer empty except: traceback.print_exc() finally: try: # don't wait longer than next expiry time qwait = buffer[0] - time.time() if qwait < 0: qwait = 0 except IndexError: # buffer empty, can wait forever qwait = None # debug message # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) class OrderedWebhook(RunForeverAsThread): def __init__(self, bot, handle=None): self._bot = bot self._collectloop = CollectLoop(_infer_handler_function(bot, handle)) self._orderer = Orderer(lambda update: self._collectloop.input_queue.put(_extract_message(update)[1])) # feed messages to collect loop def run_forever(self, *args, **kwargs): """ :type maxhold: float :param maxhold: The maximum number of seconds an update is held waiting for a not-yet-arrived smaller ``update_id``. When this number of seconds is up, the update is delivered to the message-handling function even if some smaller ``update_id``\s have not yet arrived. If those smaller ``update_id``\s arrive at some later time, they are discarded. Calling this method will block forever. Use :meth:`.run_as_thread` to run it non-blockingly. """ # feed events to collect loop self._bot.scheduler.on_event(self._collectloop.input_queue.put) self._bot.scheduler.run_as_thread() self._orderer.run_as_thread(*args, **kwargs) self._collectloop.run_forever() def feed(self, data): """ :param data: One of these: - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8) representing a JSON-serialized `Update `_ object. - a ``dict`` representing an Update object. """ update = _dictify(data) self._orderer.input_queue.put(update)