1import time
2import traceback
3import threading
4import logging
5import collections
6import re
7import inspect
8from functools import partial
9from . import filtering, exception
10from . import (
11    flavor, chat_flavors, inline_flavors, is_event,
12    message_identifier, origin_identifier)
13
14try:
15    import Queue as queue
16except ImportError:
17    import queue
18
19
20class Microphone(object):
21    def __init__(self):
22        self._queues = set()
23        self._lock = threading.Lock()
24
25    def _locked(func):
26        def k(self, *args, **kwargs):
27            with self._lock:
28                return func(self, *args, **kwargs)
29        return k
30
31    @_locked
32    def add(self, q):
33        self._queues.add(q)
34
35    @_locked
36    def remove(self, q):
37        self._queues.remove(q)
38
39    @_locked
40    def send(self, msg):
41        for q in self._queues:
42            try:
43                q.put_nowait(msg)
44            except queue.Full:
45                traceback.print_exc()
46
47
48class Listener(object):
49    def __init__(self, mic, q):
50        self._mic = mic
51        self._queue = q
52        self._patterns = []
53
54    def __del__(self):
55        self._mic.remove(self._queue)
56
57    def capture(self, pattern):
58        """
59        Add a pattern to capture.
60
61        :param pattern: a list of templates.
62
63        A template may be a function that:
64            - takes one argument - a message
65            - returns ``True`` to indicate a match
66
67        A template may also be a dictionary whose:
68            - **keys** are used to *select* parts of message. Can be strings or
69              regular expressions (as obtained by ``re.compile()``)
70            - **values** are used to match against the selected parts. Can be
71              typical data or a function.
72
73        All templates must produce a match for a message to be considered a match.
74        """
75        self._patterns.append(pattern)
76
77    def wait(self):
78        """
79        Block until a matched message appears.
80        """
81        if not self._patterns:
82            raise RuntimeError('Listener has nothing to capture')
83
84        while 1:
85            msg = self._queue.get(block=True)
86
87            if any(map(lambda p: filtering.match_all(msg, p), self._patterns)):
88                return msg
89
90
91class Sender(object):
92    """
93    When you are dealing with a particular chat, it is tedious to have to supply
94    the same ``chat_id`` every time to send a message, or to send anything.
95
96    This object is a proxy to a bot's ``send*`` and ``forwardMessage`` methods,
97    automatically fills in a fixed chat id for you. Available methods have
98    identical signatures as those of the underlying bot, **except there is no need
99    to supply the aforementioned** ``chat_id``:
100
101    - :meth:`.Bot.sendMessage`
102    - :meth:`.Bot.forwardMessage`
103    - :meth:`.Bot.sendPhoto`
104    - :meth:`.Bot.sendAudio`
105    - :meth:`.Bot.sendDocument`
106    - :meth:`.Bot.sendSticker`
107    - :meth:`.Bot.sendVideo`
108    - :meth:`.Bot.sendVoice`
109    - :meth:`.Bot.sendVideoNote`
110    - :meth:`.Bot.sendMediaGroup`
111    - :meth:`.Bot.sendLocation`
112    - :meth:`.Bot.sendVenue`
113    - :meth:`.Bot.sendContact`
114    - :meth:`.Bot.sendGame`
115    - :meth:`.Bot.sendChatAction`
116    """
117
118    def __init__(self, bot, chat_id):
119        for method in ['sendMessage',
120                       'forwardMessage',
121                       'sendPhoto',
122                       'sendAudio',
123                       'sendDocument',
124                       'sendSticker',
125                       'sendVideo',
126                       'sendVoice',
127                       'sendVideoNote',
128                       'sendMediaGroup',
129                       'sendLocation',
130                       'sendVenue',
131                       'sendContact',
132                       'sendGame',
133                       'sendChatAction',]:
134            setattr(self, method, partial(getattr(bot, method), chat_id))
135            # Essentially doing:
136            #   self.sendMessage = partial(bot.sendMessage, chat_id)
137
138
139class Administrator(object):
140    """
141    When you are dealing with a particular chat, it is tedious to have to supply
142    the same ``chat_id`` every time to get a chat's info or to perform administrative
143    tasks.
144
145    This object is a proxy to a bot's chat administration methods,
146    automatically fills in a fixed chat id for you. Available methods have
147    identical signatures as those of the underlying bot, **except there is no need
148    to supply the aforementioned** ``chat_id``:
149
150    - :meth:`.Bot.kickChatMember`
151    - :meth:`.Bot.unbanChatMember`
152    - :meth:`.Bot.restrictChatMember`
153    - :meth:`.Bot.promoteChatMember`
154    - :meth:`.Bot.exportChatInviteLink`
155    - :meth:`.Bot.setChatPhoto`
156    - :meth:`.Bot.deleteChatPhoto`
157    - :meth:`.Bot.setChatTitle`
158    - :meth:`.Bot.setChatDescription`
159    - :meth:`.Bot.pinChatMessage`
160    - :meth:`.Bot.unpinChatMessage`
161    - :meth:`.Bot.leaveChat`
162    - :meth:`.Bot.getChat`
163    - :meth:`.Bot.getChatAdministrators`
164    - :meth:`.Bot.getChatMembersCount`
165    - :meth:`.Bot.getChatMember`
166    - :meth:`.Bot.setChatStickerSet`
167    - :meth:`.Bot.deleteChatStickerSet`
168    """
169
170    def __init__(self, bot, chat_id):
171        for method in ['kickChatMember',
172                       'unbanChatMember',
173                       'restrictChatMember',
174                       'promoteChatMember',
175                       'exportChatInviteLink',
176                       'setChatPhoto',
177                       'deleteChatPhoto',
178                       'setChatTitle',
179                       'setChatDescription',
180                       'pinChatMessage',
181                       'unpinChatMessage',
182                       'leaveChat',
183                       'getChat',
184                       'getChatAdministrators',
185                       'getChatMembersCount',
186                       'getChatMember',
187                       'setChatStickerSet',
188                       'deleteChatStickerSet']:
189            setattr(self, method, partial(getattr(bot, method), chat_id))
190
191
192class Editor(object):
193    """
194    If you want to edit a message over and over, it is tedious to have to supply
195    the same ``msg_identifier`` every time.
196
197    This object is a proxy to a bot's message-editing methods, automatically fills
198    in a fixed message identifier for you. Available methods have
199    identical signatures as those of the underlying bot, **except there is no need
200    to supply the aforementioned** ``msg_identifier``:
201
202    - :meth:`.Bot.editMessageText`
203    - :meth:`.Bot.editMessageCaption`
204    - :meth:`.Bot.editMessageReplyMarkup`
205    - :meth:`.Bot.deleteMessage`
206    - :meth:`.Bot.editMessageLiveLocation`
207    - :meth:`.Bot.stopMessageLiveLocation`
208
209    A message's identifier can be easily extracted with :func:`telepot.message_identifier`.
210    """
211
212    def __init__(self, bot, msg_identifier):
213        """
214        :param msg_identifier:
215            a message identifier as mentioned above, or a message (whose
216            identifier will be automatically extracted).
217        """
218        # Accept dict as argument. Maybe expand this convenience to other cases in future.
219        if isinstance(msg_identifier, dict):
220            msg_identifier = message_identifier(msg_identifier)
221
222        for method in ['editMessageText',
223                       'editMessageCaption',
224                       'editMessageReplyMarkup',
225                       'deleteMessage',
226                       'editMessageLiveLocation',
227                       'stopMessageLiveLocation']:
228            setattr(self, method, partial(getattr(bot, method), msg_identifier))
229
230
231class Answerer(object):
232    """
233    When processing inline queries, ensure **at most one active thread** per user id.
234    """
235
236    def __init__(self, bot):
237        self._bot = bot
238        self._workers = {}  # map: user id --> worker thread
239        self._lock = threading.Lock()  # control access to `self._workers`
240
241    def answer(outerself, inline_query, compute_fn, *compute_args, **compute_kwargs):
242        """
243        Spawns a thread that calls ``compute fn`` (along with additional arguments
244        ``*compute_args`` and ``**compute_kwargs``), then applies the returned value to
245        :meth:`.Bot.answerInlineQuery` to answer the inline query.
246        If a preceding thread is already working for a user, that thread is cancelled,
247        thus ensuring at most one active thread per user id.
248
249        :param inline_query:
250            The inline query to be processed. The originating user is inferred from ``msg['from']['id']``.
251
252        :param compute_fn:
253            A **thread-safe** function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send.
254            May return:
255
256            - a *list* of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_
257            - a *tuple* whose first element is a list of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_,
258              followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery`
259            - a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery`
260
261        :param \*compute_args: positional arguments to ``compute_fn``
262        :param \*\*compute_kwargs: keyword arguments to ``compute_fn``
263        """
264
265        from_id = inline_query['from']['id']
266
267        class Worker(threading.Thread):
268            def __init__(innerself):
269                super(Worker, innerself).__init__()
270                innerself._cancelled = False
271
272            def cancel(innerself):
273                innerself._cancelled = True
274
275            def run(innerself):
276                try:
277                    query_id = inline_query['id']
278
279                    if innerself._cancelled:
280                        return
281
282                    # Important: compute function must be thread-safe.
283                    ans = compute_fn(*compute_args, **compute_kwargs)
284
285                    if innerself._cancelled:
286                        return
287
288                    if isinstance(ans, list):
289                        outerself._bot.answerInlineQuery(query_id, ans)
290                    elif isinstance(ans, tuple):
291                        outerself._bot.answerInlineQuery(query_id, *ans)
292                    elif isinstance(ans, dict):
293                        outerself._bot.answerInlineQuery(query_id, **ans)
294                    else:
295                        raise ValueError('Invalid answer format')
296                finally:
297                    with outerself._lock:
298                        # Delete only if I have NOT been cancelled.
299                        if not innerself._cancelled:
300                            del outerself._workers[from_id]
301
302                        # If I have been cancelled, that position in `outerself._workers`
303                        # no longer belongs to me. I should not delete that key.
304
305        # Several threads may access `outerself._workers`. Use `outerself._lock` to protect.
306        with outerself._lock:
307            if from_id in outerself._workers:
308                outerself._workers[from_id].cancel()
309
310            outerself._workers[from_id] = Worker()
311            outerself._workers[from_id].start()
312
313
314class AnswererMixin(object):
315    """
316    Install an :class:`.Answerer` to handle inline query.
317    """
318    Answerer = Answerer  # let subclass customize Answerer class
319
320    def __init__(self, *args, **kwargs):
321        self._answerer = self.Answerer(self.bot)
322        super(AnswererMixin, self).__init__(*args, **kwargs)
323
324    @property
325    def answerer(self):
326        return self._answerer
327
328
329class CallbackQueryCoordinator(object):
330    def __init__(self, id, origin_set, enable_chat, enable_inline):
331        """
332        :param origin_set:
333            Callback query whose origin belongs to this set will be captured
334
335        :param enable_chat:
336            - ``False``: Do not intercept *chat-originated* callback query
337            - ``True``: Do intercept
338            - Notifier function: Do intercept and call the notifier function
339              on adding or removing an origin
340
341        :param enable_inline:
342            Same meaning as ``enable_chat``, but apply to *inline-originated*
343            callback query
344
345        Notifier functions should have the signature ``notifier(origin, id, adding)``:
346
347        - On adding an origin, ``notifier(origin, my_id, True)`` will be called.
348        - On removing an origin, ``notifier(origin, my_id, False)`` will be called.
349        """
350        self._id = id
351        self._origin_set = origin_set
352
353        def dissolve(enable):
354            if not enable:
355                return False, None
356            elif enable is True:
357                return True, None
358            elif callable(enable):
359                return True, enable
360            else:
361                raise ValueError()
362
363        self._enable_chat, self._chat_notify = dissolve(enable_chat)
364        self._enable_inline, self._inline_notify = dissolve(enable_inline)
365
366    def configure(self, listener):
367        """
368        Configure a :class:`.Listener` to capture callback query
369        """
370        listener.capture([
371            lambda msg: flavor(msg) == 'callback_query',
372            {'message': self._chat_origin_included}
373        ])
374
375        listener.capture([
376            lambda msg: flavor(msg) == 'callback_query',
377            {'inline_message_id': self._inline_origin_included}
378        ])
379
380    def _chat_origin_included(self, msg):
381        try:
382            return (msg['chat']['id'], msg['message_id']) in self._origin_set
383        except KeyError:
384            return False
385
386    def _inline_origin_included(self, inline_message_id):
387        return (inline_message_id,) in self._origin_set
388
389    def _rectify(self, msg_identifier):
390        if isinstance(msg_identifier, tuple):
391            if len(msg_identifier) == 2:
392                return msg_identifier, self._chat_notify
393            elif len(msg_identifier) == 1:
394                return msg_identifier, self._inline_notify
395            else:
396                raise ValueError()
397        else:
398            return (msg_identifier,), self._inline_notify
399
400    def capture_origin(self, msg_identifier, notify=True):
401        msg_identifier, notifier = self._rectify(msg_identifier)
402        self._origin_set.add(msg_identifier)
403        notify and notifier and notifier(msg_identifier, self._id, True)
404
405    def uncapture_origin(self, msg_identifier, notify=True):
406        msg_identifier, notifier = self._rectify(msg_identifier)
407        self._origin_set.discard(msg_identifier)
408        notify and notifier and notifier(msg_identifier, self._id, False)
409
410    def _contains_callback_data(self, message_kw):
411        def contains(obj, key):
412            if isinstance(obj, dict):
413                return key in obj
414            else:
415                return hasattr(obj, key)
416
417        if contains(message_kw, 'reply_markup'):
418            reply_markup = filtering.pick(message_kw, 'reply_markup')
419            if contains(reply_markup, 'inline_keyboard'):
420                inline_keyboard = filtering.pick(reply_markup, 'inline_keyboard')
421                for array in inline_keyboard:
422                    if any(filter(lambda button: contains(button, 'callback_data'), array)):
423                        return True
424        return False
425
426    def augment_send(self, send_func):
427        """
428        :param send_func:
429            a function that sends messages, such as :meth:`.Bot.send\*`
430
431        :return:
432            a function that wraps around ``send_func`` and examines whether the
433            sent message contains an inline keyboard with callback data. If so,
434            future callback query originating from the sent message will be captured.
435        """
436        def augmented(*aa, **kw):
437            sent = send_func(*aa, **kw)
438
439            if self._enable_chat and self._contains_callback_data(kw):
440                self.capture_origin(message_identifier(sent))
441
442            return sent
443        return augmented
444
445    def augment_edit(self, edit_func):
446        """
447        :param edit_func:
448            a function that edits messages, such as :meth:`.Bot.edit*`
449
450        :return:
451            a function that wraps around ``edit_func`` and examines whether the
452            edited message contains an inline keyboard with callback data. If so,
453            future callback query originating from the edited message will be captured.
454            If not, such capturing will be stopped.
455        """
456        def augmented(msg_identifier, *aa, **kw):
457            edited = edit_func(msg_identifier, *aa, **kw)
458
459            if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat):
460                if self._contains_callback_data(kw):
461                    self.capture_origin(msg_identifier)
462                else:
463                    self.uncapture_origin(msg_identifier)
464
465            return edited
466        return augmented
467
468    def augment_delete(self, delete_func):
469        """
470        :param delete_func:
471            a function that deletes messages, such as :meth:`.Bot.deleteMessage`
472
473        :return:
474            a function that wraps around ``delete_func`` and stops capturing
475            callback query originating from that deleted message.
476        """
477        def augmented(msg_identifier, *aa, **kw):
478            deleted = delete_func(msg_identifier, *aa, **kw)
479
480            if deleted is True:
481                self.uncapture_origin(msg_identifier)
482
483            return deleted
484        return augmented
485
486    def augment_on_message(self, handler):
487        """
488        :param handler:
489            an ``on_message()`` handler function
490
491        :return:
492            a function that wraps around ``handler`` and examines whether the
493            incoming message is a chosen inline result with an ``inline_message_id``
494            field. If so, future callback query originating from this chosen
495            inline result will be captured.
496        """
497        def augmented(msg):
498            if (self._enable_inline
499                    and flavor(msg) == 'chosen_inline_result'
500                    and 'inline_message_id' in msg):
501                inline_message_id = msg['inline_message_id']
502                self.capture_origin(inline_message_id)
503
504            return handler(msg)
505        return augmented
506
507    def augment_bot(self, bot):
508        """
509        :return:
510            a proxy to ``bot`` with these modifications:
511
512            - all ``send*`` methods augmented by :meth:`augment_send`
513            - all ``edit*`` methods augmented by :meth:`augment_edit`
514            - ``deleteMessage()`` augmented by :meth:`augment_delete`
515            - all other public methods, including properties, copied unchanged
516        """
517        # Because a plain object cannot be set attributes, we need a class.
518        class BotProxy(object):
519            pass
520
521        proxy = BotProxy()
522
523        send_methods = ['sendMessage',
524                        'forwardMessage',
525                        'sendPhoto',
526                        'sendAudio',
527                        'sendDocument',
528                        'sendSticker',
529                        'sendVideo',
530                        'sendVoice',
531                        'sendVideoNote',
532                        'sendLocation',
533                        'sendVenue',
534                        'sendContact',
535                        'sendGame',
536                        'sendInvoice',
537                        'sendChatAction',]
538
539        for method in send_methods:
540            setattr(proxy, method, self.augment_send(getattr(bot, method)))
541
542        edit_methods = ['editMessageText',
543                        'editMessageCaption',
544                        'editMessageReplyMarkup',]
545
546        for method in edit_methods:
547            setattr(proxy, method, self.augment_edit(getattr(bot, method)))
548
549        delete_methods = ['deleteMessage']
550
551        for method in delete_methods:
552            setattr(proxy, method, self.augment_delete(getattr(bot, method)))
553
554        def public_untouched(nv):
555            name, value = nv
556            return (not name.startswith('_')
557                    and name not in send_methods + edit_methods + delete_methods)
558
559        for name, value in filter(public_untouched, inspect.getmembers(bot)):
560            setattr(proxy, name, value)
561
562        return proxy
563
564
565class SafeDict(dict):
566    """
567    A subclass of ``dict``, thread-safety added::
568
569        d = SafeDict()  # Thread-safe operations include:
570        d['a'] = 3      # key assignment
571        d['a']          # key retrieval
572        del d['a']      # key deletion
573    """
574
575    def __init__(self, *args, **kwargs):
576        super(SafeDict, self).__init__(*args, **kwargs)
577        self._lock = threading.Lock()
578
579    def _locked(func):
580        def k(self, *args, **kwargs):
581            with self._lock:
582                return func(self, *args, **kwargs)
583        return k
584
585    @_locked
586    def __getitem__(self, key):
587        return super(SafeDict, self).__getitem__(key)
588
589    @_locked
590    def __setitem__(self, key, value):
591        return super(SafeDict, self).__setitem__(key, value)
592
593    @_locked
594    def __delitem__(self, key):
595        return super(SafeDict, self).__delitem__(key)
596
597
598_cqc_origins = SafeDict()
599
600class InterceptCallbackQueryMixin(object):
601    """
602    Install a :class:`.CallbackQueryCoordinator` to capture callback query
603    dynamically.
604
605    Using this mixin has one consequence. The :meth:`self.bot` property no longer
606    returns the original :class:`.Bot` object. Instead, it returns an augmented
607    version of the :class:`.Bot` (augmented by :class:`.CallbackQueryCoordinator`).
608    The original :class:`.Bot` can be accessed with ``self.__bot`` (double underscore).
609    """
610    CallbackQueryCoordinator = CallbackQueryCoordinator
611
612    def __init__(self, intercept_callback_query, *args, **kwargs):
613        """
614        :param intercept_callback_query:
615            a 2-tuple (enable_chat, enable_inline) to pass to
616            :class:`.CallbackQueryCoordinator`
617        """
618        global _cqc_origins
619
620        # Restore origin set to CallbackQueryCoordinator
621        if self.id in _cqc_origins:
622            origin_set = _cqc_origins[self.id]
623        else:
624            origin_set = set()
625            _cqc_origins[self.id] = origin_set
626
627        if isinstance(intercept_callback_query, tuple):
628            cqc_enable = intercept_callback_query
629        else:
630            cqc_enable = (intercept_callback_query,) * 2
631
632        self._callback_query_coordinator = self.CallbackQueryCoordinator(self.id, origin_set, *cqc_enable)
633        cqc = self._callback_query_coordinator
634        cqc.configure(self.listener)
635
636        self.__bot = self._bot  # keep original version of bot
637        self._bot = cqc.augment_bot(self._bot)  # modify send* and edit* methods
638        self.on_message = cqc.augment_on_message(self.on_message)  # modify on_message()
639
640        super(InterceptCallbackQueryMixin, self).__init__(*args, **kwargs)
641
642    def __del__(self):
643        global _cqc_origins
644        if self.id in _cqc_origins and not _cqc_origins[self.id]:
645            del _cqc_origins[self.id]
646            # Remove empty set from dictionary
647
648    @property
649    def callback_query_coordinator(self):
650        return self._callback_query_coordinator
651
652
653class IdleEventCoordinator(object):
654    def __init__(self, scheduler, timeout):
655        self._scheduler = scheduler
656        self._timeout_seconds = timeout
657        self._timeout_event = None
658
659    def refresh(self):
660        """ Refresh timeout timer """
661        try:
662            if self._timeout_event:
663                self._scheduler.cancel(self._timeout_event)
664
665        # Timeout event has been popped from queue prematurely
666        except exception.EventNotFound:
667            pass
668
669        # Ensure a new event is scheduled always
670        finally:
671            self._timeout_event = self._scheduler.event_later(
672                                      self._timeout_seconds,
673                                      ('_idle', {'seconds': self._timeout_seconds}))
674
675    def augment_on_message(self, handler):
676        """
677        :return:
678            a function wrapping ``handler`` to refresh timer for every
679            non-event message
680        """
681        def augmented(msg):
682            # Reset timer if this is an external message
683            is_event(msg) or self.refresh()
684
685            # Ignore timeout event that have been popped from queue prematurely
686            if flavor(msg) == '_idle' and msg is not self._timeout_event.data:
687                return
688
689            return handler(msg)
690        return augmented
691
692    def augment_on_close(self, handler):
693        """
694        :return:
695            a function wrapping ``handler`` to cancel timeout event
696        """
697        def augmented(ex):
698            try:
699                if self._timeout_event:
700                    self._scheduler.cancel(self._timeout_event)
701                    self._timeout_event = None
702            # This closing may have been caused by my own timeout, in which case
703            # the timeout event can no longer be found in the scheduler.
704            except exception.EventNotFound:
705                self._timeout_event = None
706            return handler(ex)
707        return augmented
708
709
710class IdleTerminateMixin(object):
711    """
712    Install an :class:`.IdleEventCoordinator` to manage idle timeout. Also define
713    instance method ``on__idle()`` to handle idle timeout events.
714    """
715    IdleEventCoordinator = IdleEventCoordinator
716
717    def __init__(self, timeout, *args, **kwargs):
718        self._idle_event_coordinator = self.IdleEventCoordinator(self.scheduler, timeout)
719        idlec = self._idle_event_coordinator
720        idlec.refresh()  # start timer
721        self.on_message = idlec.augment_on_message(self.on_message)
722        self.on_close = idlec.augment_on_close(self.on_close)
723        super(IdleTerminateMixin, self).__init__(*args, **kwargs)
724
725    @property
726    def idle_event_coordinator(self):
727        return self._idle_event_coordinator
728
729    def on__idle(self, event):
730        """
731        Raise an :class:`.IdleTerminate` to close the delegate.
732        """
733        raise exception.IdleTerminate(event['_idle']['seconds'])
734
735
736class StandardEventScheduler(object):
737    """
738    A proxy to the underlying :class:`.Bot`\'s scheduler, this object implements
739    the *standard event format*. A standard event looks like this::
740
741        {'_flavor': {
742            'source': {
743                'space': event_space, 'id': source_id}
744            'custom_key1': custom_value1,
745            'custom_key2': custom_value2,
746             ... }}
747
748    - There is a single top-level key indicating the flavor, starting with an _underscore.
749    - On the second level, there is a ``source`` key indicating the event source.
750    - An event source consists of an *event space* and a *source id*.
751    - An event space is shared by all delegates in a group. Source id simply refers
752      to a delegate's id. They combine to ensure a delegate is always able to capture
753      its own events, while its own events would not be mistakenly captured by others.
754
755    Events scheduled through this object always have the second-level ``source`` key fixed,
756    while the flavor and other data may be customized.
757    """
758    def __init__(self, scheduler, event_space, source_id):
759        self._base = scheduler
760        self._event_space = event_space
761        self._source_id = source_id
762
763    @property
764    def event_space(self):
765        return self._event_space
766
767    def configure(self, listener):
768        """
769        Configure a :class:`.Listener` to capture events with this object's
770        event space and source id.
771        """
772        listener.capture([{re.compile('^_.+'): {'source': {'space': self._event_space, 'id': self._source_id}}}])
773
774    def make_event_data(self, flavor, data):
775        """
776        Marshall ``flavor`` and ``data`` into a standard event.
777        """
778        if not flavor.startswith('_'):
779            raise ValueError('Event flavor must start with _underscore')
780
781        d = {'source': {'space': self._event_space, 'id': self._source_id}}
782        d.update(data)
783        return {flavor: d}
784
785    def event_at(self, when, data_tuple):
786        """
787        Schedule an event to be emitted at a certain time.
788
789        :param when: an absolute timestamp
790        :param data_tuple: a 2-tuple (flavor, data)
791        :return: an event object, useful for cancelling.
792        """
793        return self._base.event_at(when, self.make_event_data(*data_tuple))
794
795    def event_later(self, delay, data_tuple):
796        """
797        Schedule an event to be emitted after a delay.
798
799        :param delay: number of seconds
800        :param data_tuple: a 2-tuple (flavor, data)
801        :return: an event object, useful for cancelling.
802        """
803        return self._base.event_later(delay, self.make_event_data(*data_tuple))
804
805    def event_now(self, data_tuple):
806        """
807        Schedule an event to be emitted now.
808
809        :param data_tuple: a 2-tuple (flavor, data)
810        :return: an event object, useful for cancelling.
811        """
812        return self._base.event_now(self.make_event_data(*data_tuple))
813
814    def cancel(self, event):
815        """ Cancel an event. """
816        return self._base.cancel(event)
817
818
819class StandardEventMixin(object):
820    """
821    Install a :class:`.StandardEventScheduler`.
822    """
823    StandardEventScheduler = StandardEventScheduler
824
825    def __init__(self, event_space, *args, **kwargs):
826        self._scheduler = self.StandardEventScheduler(self.bot.scheduler, event_space, self.id)
827        self._scheduler.configure(self.listener)
828        super(StandardEventMixin, self).__init__(*args, **kwargs)
829
830    @property
831    def scheduler(self):
832        return self._scheduler
833
834
835class ListenerContext(object):
836    def __init__(self, bot, context_id, *args, **kwargs):
837        # Initialize members before super() so mixin could use them.
838        self._bot = bot
839        self._id = context_id
840        self._listener = bot.create_listener()
841        super(ListenerContext, self).__init__(*args, **kwargs)
842
843    @property
844    def bot(self):
845        """
846        The underlying :class:`.Bot` or an augmented version thereof
847        """
848        return self._bot
849
850    @property
851    def id(self):
852        return self._id
853
854    @property
855    def listener(self):
856        """ See :class:`.Listener` """
857        return self._listener
858
859
860class ChatContext(ListenerContext):
861    def __init__(self, bot, context_id, *args, **kwargs):
862        super(ChatContext, self).__init__(bot, context_id, *args, **kwargs)
863        self._chat_id = context_id
864        self._sender = Sender(self.bot, self._chat_id)
865        self._administrator = Administrator(self.bot, self._chat_id)
866
867    @property
868    def chat_id(self):
869        return self._chat_id
870
871    @property
872    def sender(self):
873        """ A :class:`.Sender` for this chat """
874        return self._sender
875
876    @property
877    def administrator(self):
878        """ An :class:`.Administrator` for this chat """
879        return self._administrator
880
881
882class UserContext(ListenerContext):
883    def __init__(self, bot, context_id, *args, **kwargs):
884        super(UserContext, self).__init__(bot, context_id, *args, **kwargs)
885        self._user_id = context_id
886        self._sender = Sender(self.bot, self._user_id)
887
888    @property
889    def user_id(self):
890        return self._user_id
891
892    @property
893    def sender(self):
894        """ A :class:`.Sender` for this user """
895        return self._sender
896
897
898class CallbackQueryOriginContext(ListenerContext):
899    def __init__(self, bot, context_id, *args, **kwargs):
900        super(CallbackQueryOriginContext, self).__init__(bot, context_id, *args, **kwargs)
901        self._origin = context_id
902        self._editor = Editor(self.bot, self._origin)
903
904    @property
905    def origin(self):
906        """ Mesasge identifier of callback query's origin """
907        return self._origin
908
909    @property
910    def editor(self):
911        """ An :class:`.Editor` to the originating message """
912        return self._editor
913
914
915class InvoiceContext(ListenerContext):
916    def __init__(self, bot, context_id, *args, **kwargs):
917        super(InvoiceContext, self).__init__(bot, context_id, *args, **kwargs)
918        self._payload = context_id
919
920    @property
921    def payload(self):
922        return self._payload
923
924
925def openable(cls):
926    """
927    A class decorator to fill in certain methods and properties to ensure
928    a class can be used by :func:`.create_open`.
929
930    These instance methods and property will be added, if not defined
931    by the class:
932
933    - ``open(self, initial_msg, seed)``
934    - ``on_message(self, msg)``
935    - ``on_close(self, ex)``
936    - ``close(self, ex=None)``
937    - property ``listener``
938    """
939
940    def open(self, initial_msg, seed):
941        pass
942
943    def on_message(self, msg):
944        raise NotImplementedError()
945
946    def on_close(self, ex):
947        logging.error('on_close() called due to %s: %s', type(ex).__name__, ex)
948
949    def close(self, ex=None):
950        raise ex if ex else exception.StopListening()
951
952    @property
953    def listener(self):
954        raise NotImplementedError()
955
956    def ensure_method(name, fn):
957        if getattr(cls, name, None) is None:
958            setattr(cls, name, fn)
959
960    # set attribute if no such attribute
961    ensure_method('open', open)
962    ensure_method('on_message', on_message)
963    ensure_method('on_close', on_close)
964    ensure_method('close', close)
965    ensure_method('listener', listener)
966
967    return cls
968
969
970class Router(object):
971    """
972    Map a message to a handler function, using a **key function** and
973    a **routing table** (dictionary).
974
975    A *key function* digests a message down to a value. This value is treated
976    as a key to the *routing table* to look up a corresponding handler function.
977    """
978
979    def __init__(self, key_function, routing_table):
980        """
981        :param key_function:
982            A function that takes one argument (the message) and returns
983            one of the following:
984
985            - a key to the routing table
986            - a 1-tuple (key,)
987            - a 2-tuple (key, (positional, arguments, ...))
988            - a 3-tuple (key, (positional, arguments, ...), {keyword: arguments, ...})
989
990            Extra arguments, if returned, will be applied to the handler function
991            after using the key to look up the routing table.
992
993        :param routing_table:
994            A dictionary of ``{key: handler}``. A ``None`` key acts as a default
995            catch-all. If the key being looked up does not exist in the routing
996            table, the ``None`` key and its corresponding handler is used.
997        """
998        super(Router, self).__init__()
999        self.key_function = key_function
1000        self.routing_table = routing_table
1001
1002    def map(self, msg):
1003        """
1004        Apply key function to ``msg`` to obtain a key. Return the routing table entry.
1005        """
1006        k = self.key_function(msg)
1007        key = k[0] if isinstance(k, (tuple, list)) else k
1008        return self.routing_table[key]
1009
1010    def route(self, msg, *aa, **kw):
1011        """
1012        Apply key function to ``msg`` to obtain a key, look up routing table
1013        to obtain a handler function, then call the handler function with
1014        positional and keyword arguments, if any is returned by the key function.
1015
1016        ``*aa`` and ``**kw`` are dummy placeholders for easy chaining.
1017        Regardless of any number of arguments returned by the key function,
1018        multi-level routing may be achieved like this::
1019
1020            top_router.routing_table['key1'] = sub_router1.route
1021            top_router.routing_table['key2'] = sub_router2.route
1022        """
1023        k = self.key_function(msg)
1024
1025        if isinstance(k, (tuple, list)):
1026            key, args, kwargs = {1: tuple(k) + ((),{}),
1027                                 2: tuple(k) + ({},),
1028                                 3: tuple(k),}[len(k)]
1029        else:
1030            key, args, kwargs = k, (), {}
1031
1032        try:
1033            fn = self.routing_table[key]
1034        except KeyError as e:
1035            # Check for default handler, key=None
1036            if None in self.routing_table:
1037                fn = self.routing_table[None]
1038            else:
1039                raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args))
1040
1041        return fn(msg, *args, **kwargs)
1042
1043
1044class DefaultRouterMixin(object):
1045    """
1046    Install a default :class:`.Router` and the instance method ``on_message()``.
1047    """
1048    def __init__(self, *args, **kwargs):
1049        self._router = Router(flavor, {'chat': lambda msg: self.on_chat_message(msg),
1050                                       'callback_query': lambda msg: self.on_callback_query(msg),
1051                                       'inline_query': lambda msg: self.on_inline_query(msg),
1052                                       'chosen_inline_result': lambda msg: self.on_chosen_inline_result(msg),
1053                                       'shipping_query': lambda msg: self.on_shipping_query(msg),
1054                                       'pre_checkout_query': lambda msg: self.on_pre_checkout_query(msg),
1055                                       '_idle': lambda event: self.on__idle(event)})
1056                                       # use lambda to delay evaluation of self.on_ZZZ to runtime because
1057                                       # I don't want to require defining all methods right here.
1058
1059        super(DefaultRouterMixin, self).__init__(*args, **kwargs)
1060
1061    @property
1062    def router(self):
1063        return self._router
1064
1065    def on_message(self, msg):
1066        """ Call :meth:`.Router.route` to handle the message. """
1067        self._router.route(msg)
1068
1069
1070@openable
1071class Monitor(ListenerContext, DefaultRouterMixin):
1072    def __init__(self, seed_tuple, capture, **kwargs):
1073        """
1074        A delegate that never times-out, probably doing some kind of background monitoring
1075        in the application. Most naturally paired with :func:`.per_application`.
1076
1077        :param capture: a list of patterns for :class:`.Listener` to capture
1078        """
1079        bot, initial_msg, seed = seed_tuple
1080        super(Monitor, self).__init__(bot, seed, **kwargs)
1081
1082        for pattern in capture:
1083            self.listener.capture(pattern)
1084
1085
1086@openable
1087class ChatHandler(ChatContext,
1088                  DefaultRouterMixin,
1089                  StandardEventMixin,
1090                  IdleTerminateMixin):
1091    def __init__(self, seed_tuple,
1092                 include_callback_query=False, **kwargs):
1093        """
1094        A delegate to handle a chat.
1095        """
1096        bot, initial_msg, seed = seed_tuple
1097        super(ChatHandler, self).__init__(bot, seed, **kwargs)
1098
1099        self.listener.capture([{'chat': {'id': self.chat_id}}])
1100
1101        if include_callback_query:
1102            self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}])
1103
1104
1105@openable
1106class UserHandler(UserContext,
1107                  DefaultRouterMixin,
1108                  StandardEventMixin,
1109                  IdleTerminateMixin):
1110    def __init__(self, seed_tuple,
1111                 include_callback_query=False,
1112                 flavors=chat_flavors+inline_flavors, **kwargs):
1113        """
1114        A delegate to handle a user's actions.
1115
1116        :param flavors:
1117            A list of flavors to capture. ``all`` covers all flavors.
1118        """
1119        bot, initial_msg, seed = seed_tuple
1120        super(UserHandler, self).__init__(bot, seed, **kwargs)
1121
1122        if flavors == 'all':
1123            self.listener.capture([{'from': {'id': self.user_id}}])
1124        else:
1125            self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}])
1126
1127        if include_callback_query:
1128            self.listener.capture([{'message': {'chat': {'id': self.user_id}}}])
1129
1130
1131class InlineUserHandler(UserHandler):
1132    def __init__(self, seed_tuple, **kwargs):
1133        """
1134        A delegate to handle a user's inline-related actions.
1135        """
1136        super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs)
1137
1138
1139@openable
1140class CallbackQueryOriginHandler(CallbackQueryOriginContext,
1141                                 DefaultRouterMixin,
1142                                 StandardEventMixin,
1143                                 IdleTerminateMixin):
1144    def __init__(self, seed_tuple, **kwargs):
1145        """
1146        A delegate to handle callback query from one origin.
1147        """
1148        bot, initial_msg, seed = seed_tuple
1149        super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs)
1150
1151        self.listener.capture([
1152            lambda msg:
1153                flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin
1154        ])
1155
1156
1157@openable
1158class InvoiceHandler(InvoiceContext,
1159                     DefaultRouterMixin,
1160                     StandardEventMixin,
1161                     IdleTerminateMixin):
1162    def __init__(self, seed_tuple, **kwargs):
1163        """
1164        A delegate to handle messages related to an invoice.
1165        """
1166        bot, initial_msg, seed = seed_tuple
1167        super(InvoiceHandler, self).__init__(bot, seed, **kwargs)
1168
1169        self.listener.capture([{'invoice_payload': self.payload}])
1170        self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}])
1171