diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-07-19 10:42:15 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-07-19 10:42:15 +0300 |
| commit | 5a636abf7e98a5687811167917d0e07103599bc8 (patch) | |
| tree | 682f8d70d194c6bf02b750780f05689fcb83ba5a /telepot/loop.py | |
| parent | e1ae18d1bc9e26f8ab817ca1c1e4e271badf02ad (diff) | |
Vendor the unmaintained telepot dependency
Diffstat (limited to 'telepot/loop.py')
| -rw-r--r-- | telepot/loop.py | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/telepot/loop.py b/telepot/loop.py new file mode 100644 index 0000000..4b1331b --- /dev/null +++ b/telepot/loop.py @@ -0,0 +1,313 @@ +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']) + 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 <https://core.telegram.org/bots/api#update>`_ object. + - a ``dict`` representing an Update object. + """ + update = _dictify(data) + self._orderer.input_queue.put(update) |
