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