behnamdowloader commited on
Commit
0408b90
·
1 Parent(s): bc7e119

Upload 42 files

Browse files
Files changed (42) hide show
  1. telepot-12.7.dist-info/INSTALLER +1 -0
  2. telepot-12.7.dist-info/METADATA +24 -0
  3. telepot-12.7.dist-info/RECORD +42 -0
  4. telepot-12.7.dist-info/REQUESTED +0 -0
  5. telepot-12.7.dist-info/WHEEL +5 -0
  6. telepot-12.7.dist-info/top_level.txt +1 -0
  7. telepot/__init__.py +1411 -0
  8. telepot/__pycache__/__init__.cpython-311.pyc +0 -0
  9. telepot/__pycache__/api.cpython-311.pyc +0 -0
  10. telepot/__pycache__/delegate.cpython-311.pyc +0 -0
  11. telepot/__pycache__/exception.cpython-311.pyc +0 -0
  12. telepot/__pycache__/filtering.cpython-311.pyc +0 -0
  13. telepot/__pycache__/hack.cpython-311.pyc +0 -0
  14. telepot/__pycache__/helper.cpython-311.pyc +0 -0
  15. telepot/__pycache__/loop.cpython-311.pyc +0 -0
  16. telepot/__pycache__/namedtuple.cpython-311.pyc +0 -0
  17. telepot/__pycache__/routing.cpython-311.pyc +0 -0
  18. telepot/__pycache__/text.cpython-311.pyc +0 -0
  19. telepot/aio/__init__.py +926 -0
  20. telepot/aio/__pycache__/__init__.cpython-311.pyc +0 -0
  21. telepot/aio/__pycache__/api.cpython-311.pyc +0 -0
  22. telepot/aio/__pycache__/delegate.cpython-311.pyc +0 -0
  23. telepot/aio/__pycache__/hack.cpython-311.pyc +0 -0
  24. telepot/aio/__pycache__/helper.cpython-311.pyc +0 -0
  25. telepot/aio/__pycache__/loop.cpython-311.pyc +0 -0
  26. telepot/aio/__pycache__/routing.cpython-311.pyc +0 -0
  27. telepot/aio/api.py +168 -0
  28. telepot/aio/delegate.py +106 -0
  29. telepot/aio/hack.py +36 -0
  30. telepot/aio/helper.py +372 -0
  31. telepot/aio/loop.py +205 -0
  32. telepot/aio/routing.py +46 -0
  33. telepot/api.py +164 -0
  34. telepot/delegate.py +420 -0
  35. telepot/exception.py +111 -0
  36. telepot/filtering.py +34 -0
  37. telepot/hack.py +16 -0
  38. telepot/helper.py +1170 -0
  39. telepot/loop.py +313 -0
  40. telepot/namedtuple.py +865 -0
  41. telepot/routing.py +223 -0
  42. telepot/text.py +88 -0
telepot-12.7.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
telepot-12.7.dist-info/METADATA ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: telepot
3
+ Version: 12.7
4
+ Summary: Python framework for Telegram Bot API
5
+ Home-page: https://github.com/nickoala/telepot
6
+ Author: Nick Lee
7
+ Author-email: lee1nick@yahoo.ca
8
+ License: MIT
9
+ Keywords: telegram bot api python wrapper
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Topic :: Communications :: Chat
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 2.7
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.2
18
+ Classifier: Programming Language :: Python :: 3.3
19
+ Classifier: Programming Language :: Python :: 3.4
20
+ Classifier: Programming Language :: Python :: 3.5
21
+ Classifier: Programming Language :: Python :: 3.6
22
+ Requires-Dist: urllib3 >=1.9.1
23
+ Requires-Dist: aiohttp >=3.0.0
24
+
telepot-12.7.dist-info/RECORD ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ telepot-12.7.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2
+ telepot-12.7.dist-info/METADATA,sha256=1eyEEbT4ubmJ1OeGcQOcjxcRmRlzObBGLp95uR8jQKA,951
3
+ telepot-12.7.dist-info/RECORD,,
4
+ telepot-12.7.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ telepot-12.7.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
6
+ telepot-12.7.dist-info/top_level.txt,sha256=9dekm1cqNkfv9c5oF3u3SFp-hxs1Pje1ERGq2CXLwiU,8
7
+ telepot/__init__.py,sha256=M1wJTZVPX0AXifBo9q1NpIVxh8MVD4sgKvagN5a-jCE,55768
8
+ telepot/__pycache__/__init__.cpython-311.pyc,,
9
+ telepot/__pycache__/api.cpython-311.pyc,,
10
+ telepot/__pycache__/delegate.cpython-311.pyc,,
11
+ telepot/__pycache__/exception.cpython-311.pyc,,
12
+ telepot/__pycache__/filtering.cpython-311.pyc,,
13
+ telepot/__pycache__/hack.cpython-311.pyc,,
14
+ telepot/__pycache__/helper.cpython-311.pyc,,
15
+ telepot/__pycache__/loop.cpython-311.pyc,,
16
+ telepot/__pycache__/namedtuple.cpython-311.pyc,,
17
+ telepot/__pycache__/routing.cpython-311.pyc,,
18
+ telepot/__pycache__/text.cpython-311.pyc,,
19
+ telepot/aio/__init__.py,sha256=6fHVtJLgvw0_VKAMYuckj7FUIvKJJ_cf1hMtx3NCJGM,40593
20
+ telepot/aio/__pycache__/__init__.cpython-311.pyc,,
21
+ telepot/aio/__pycache__/api.cpython-311.pyc,,
22
+ telepot/aio/__pycache__/delegate.cpython-311.pyc,,
23
+ telepot/aio/__pycache__/hack.cpython-311.pyc,,
24
+ telepot/aio/__pycache__/helper.cpython-311.pyc,,
25
+ telepot/aio/__pycache__/loop.cpython-311.pyc,,
26
+ telepot/aio/__pycache__/routing.cpython-311.pyc,,
27
+ telepot/aio/api.py,sha256=-g0ZDONLuCCdPyUzXrfDRsyf99f82GkhKPeMwR5nn6g,5230
28
+ telepot/aio/delegate.py,sha256=IJQIUnTQ7F_2j58mruGxAPFB2ZMp1vgUT4_AhAFmNvs,3955
29
+ telepot/aio/hack.py,sha256=XebdMVN2h0OzDuL0DrKoZjixpAarX8LkoxlEBHhl91Y,1344
30
+ telepot/aio/helper.py,sha256=p0hrw56S-N4NmMFLURbthPLiHnLsbLos1mRIFw3kzpk,13761
31
+ telepot/aio/loop.py,sha256=qJM133HMqBo6fdBoaoKnT5MWqI1dbkRtbgGHJnycEBE,7515
32
+ telepot/aio/routing.py,sha256=FOSWcSCPf1sZLTKQaon0ZPtMOOeOuV_Q_x3lARaz8fY,1594
33
+ telepot/api.py,sha256=OgcUJ4xORx-ti5PxSuKgTXTnVTaRUrbJAXRj2Y3S2ps,5246
34
+ telepot/delegate.py,sha256=am8U-EmG-ukh_DaZfjTrmXMKTxW2qb3HIvP7MtCY3v4,13933
35
+ telepot/exception.py,sha256=Eq1C3-o4T-FaWoq6yJUcV3MztVDy8sGBiM2uM4GjXQc,3416
36
+ telepot/filtering.py,sha256=OZzu8P1ca_rmKnU9VS0xu6911DM-7iWmY2jzOjVBBlY,1136
37
+ telepot/hack.py,sha256=Byvxl3VPrNzt4wiaYgzEabE_N9OtWv3Xp0yd1lJYKaI,484
38
+ telepot/helper.py,sha256=2fqhGi45Ckb9Lu82Uet73a1hGMy6idmKogY62qPdxJU,42294
39
+ telepot/loop.py,sha256=BbnFZxXd9b3_KGO_sYunJpwzahuVjP5pX9Tmb2msIxQ,11370
40
+ telepot/namedtuple.py,sha256=bRYP2NDWRhtHYcDXZBZrF94zL1ZkmKUMNYr1BgEAxE0,31588
41
+ telepot/routing.py,sha256=uW8vdNC-xwdfvSCZC3tJlvBFLxO4h5pfOBavgYcTHvM,7516
42
+ telepot/text.py,sha256=WxWiVmy9aUdVFTg8WE24WQueK7IUU0Tb5XWfn-1tQ9w,3229
telepot-12.7.dist-info/REQUESTED ADDED
File without changes
telepot-12.7.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
telepot-12.7.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ telepot
telepot/__init__.py ADDED
@@ -0,0 +1,1411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import io
3
+ import time
4
+ import json
5
+ import threading
6
+ import traceback
7
+ import collections
8
+ import bisect
9
+
10
+ try:
11
+ import Queue as queue
12
+ except ImportError:
13
+ import queue
14
+
15
+ # Patch urllib3 for sending unicode filename
16
+ from . import hack
17
+
18
+ from . import exception
19
+
20
+
21
+ __version_info__ = (12, 7)
22
+ __version__ = '.'.join(map(str, __version_info__))
23
+
24
+
25
+ def flavor(msg):
26
+ """
27
+ Return flavor of message or event.
28
+
29
+ A message's flavor may be one of these:
30
+
31
+ - ``chat``
32
+ - ``callback_query``
33
+ - ``inline_query``
34
+ - ``chosen_inline_result``
35
+ - ``shipping_query``
36
+ - ``pre_checkout_query``
37
+
38
+ An event's flavor is determined by the single top-level key.
39
+ """
40
+ if 'message_id' in msg:
41
+ return 'chat'
42
+ elif 'id' in msg and 'chat_instance' in msg:
43
+ return 'callback_query'
44
+ elif 'id' in msg and 'query' in msg:
45
+ return 'inline_query'
46
+ elif 'result_id' in msg:
47
+ return 'chosen_inline_result'
48
+ elif 'id' in msg and 'shipping_address' in msg:
49
+ return 'shipping_query'
50
+ elif 'id' in msg and 'total_amount' in msg:
51
+ return 'pre_checkout_query'
52
+ else:
53
+ top_keys = list(msg.keys())
54
+ if len(top_keys) == 1:
55
+ return top_keys[0]
56
+
57
+ raise exception.BadFlavor(msg)
58
+
59
+
60
+ chat_flavors = ['chat']
61
+ inline_flavors = ['inline_query', 'chosen_inline_result']
62
+
63
+
64
+ # def _find_first_key(d, keys):
65
+ # for k in keys:
66
+ # if k in d:
67
+ # return k
68
+ # raise KeyError('No suggested keys %s in %s' % (str(keys), str(d)))
69
+
70
+ def _find_first_key(d, keys):
71
+ for k in keys:
72
+ if k in d:
73
+ return k
74
+ return 'update_id'
75
+
76
+
77
+ all_content_types = [
78
+ 'text', 'audio', 'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note',
79
+ 'contact', 'location', 'venue', 'new_chat_member', 'left_chat_member', 'new_chat_title',
80
+ 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created',
81
+ 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
82
+ 'new_chat_members', 'invoice', 'successful_payment'
83
+ ]
84
+
85
+ def glance(msg, flavor='chat', long=False):
86
+ """
87
+ Extract "headline" info about a message.
88
+ Use parameter ``long`` to control whether a short or long tuple is returned.
89
+
90
+ When ``flavor`` is ``chat``
91
+ (``msg`` being a `Message <https://core.telegram.org/bots/api#message>`_ object):
92
+
93
+ - short: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``)
94
+ - long: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``, ``msg['date']``, ``msg['message_id']``)
95
+
96
+ *content_type* can be: ``text``, ``audio``, ``document``, ``game``, ``photo``, ``sticker``, ``video``, ``voice``,
97
+ ``video_note``, ``contact``, ``location``, ``venue``, ``new_chat_member``, ``left_chat_member``, ``new_chat_title``,
98
+ ``new_chat_photo``, ``delete_chat_photo``, ``group_chat_created``, ``supergroup_chat_created``,
99
+ ``channel_chat_created``, ``migrate_to_chat_id``, ``migrate_from_chat_id``, ``pinned_message``,
100
+ ``new_chat_members``, ``invoice``, ``successful_payment``.
101
+
102
+ When ``flavor`` is ``callback_query``
103
+ (``msg`` being a `CallbackQuery <https://core.telegram.org/bots/api#callbackquery>`_ object):
104
+
105
+ - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['data']``)
106
+
107
+ When ``flavor`` is ``inline_query``
108
+ (``msg`` being a `InlineQuery <https://core.telegram.org/bots/api#inlinequery>`_ object):
109
+
110
+ - short: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``)
111
+ - long: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``, ``msg['offset']``)
112
+
113
+ When ``flavor`` is ``chosen_inline_result``
114
+ (``msg`` being a `ChosenInlineResult <https://core.telegram.org/bots/api#choseninlineresult>`_ object):
115
+
116
+ - regardless: (``msg['result_id']``, ``msg['from']['id']``, ``msg['query']``)
117
+
118
+ When ``flavor`` is ``shipping_query``
119
+ (``msg`` being a `ShippingQuery <https://core.telegram.org/bots/api#shippingquery>`_ object):
120
+
121
+ - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``)
122
+
123
+ When ``flavor`` is ``pre_checkout_query``
124
+ (``msg`` being a `PreCheckoutQuery <https://core.telegram.org/bots/api#precheckoutquery>`_ object):
125
+
126
+ - short: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``)
127
+ - long: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``, ``msg['currency']``, ``msg['total_amount']``)
128
+ """
129
+ def gl_chat():
130
+ content_type = _find_first_key(msg, all_content_types)
131
+
132
+ if long:
133
+ return content_type, msg['chat']['type'], msg['chat']['id'], msg['date'], msg['message_id']
134
+ else:
135
+ return content_type, msg['chat']['type'], msg['chat']['id']
136
+
137
+ def gl_callback_query():
138
+ return msg['id'], msg['from']['id'], msg['data']
139
+
140
+ def gl_inline_query():
141
+ if long:
142
+ return msg['id'], msg['from']['id'], msg['query'], msg['offset']
143
+ else:
144
+ return msg['id'], msg['from']['id'], msg['query']
145
+
146
+ def gl_chosen_inline_result():
147
+ return msg['result_id'], msg['from']['id'], msg['query']
148
+
149
+ def gl_shipping_query():
150
+ return msg['id'], msg['from']['id'], msg['invoice_payload']
151
+
152
+ def gl_pre_checkout_query():
153
+ if long:
154
+ return msg['id'], msg['from']['id'], msg['invoice_payload'], msg['currency'], msg['total_amount']
155
+ else:
156
+ return msg['id'], msg['from']['id'], msg['invoice_payload']
157
+
158
+ try:
159
+ fn = {'chat': gl_chat,
160
+ 'callback_query': gl_callback_query,
161
+ 'inline_query': gl_inline_query,
162
+ 'chosen_inline_result': gl_chosen_inline_result,
163
+ 'shipping_query': gl_shipping_query,
164
+ 'pre_checkout_query': gl_pre_checkout_query}[flavor]
165
+ except KeyError:
166
+ raise exception.BadFlavor(flavor)
167
+
168
+ return fn()
169
+
170
+
171
+ def flance(msg, long=False):
172
+ """
173
+ A combination of :meth:`telepot.flavor` and :meth:`telepot.glance`,
174
+ return a 2-tuple (flavor, headline_info), where *headline_info* is whatever extracted by
175
+ :meth:`telepot.glance` depending on the message flavor and the ``long`` parameter.
176
+ """
177
+ f = flavor(msg)
178
+ g = glance(msg, flavor=f, long=long)
179
+ return f,g
180
+
181
+
182
+ def peel(event):
183
+ """
184
+ Remove an event's top-level skin (where its flavor is determined), and return
185
+ the core content.
186
+ """
187
+ return list(event.values())[0]
188
+
189
+
190
+ def fleece(event):
191
+ """
192
+ A combination of :meth:`telepot.flavor` and :meth:`telepot.peel`,
193
+ return a 2-tuple (flavor, content) of an event.
194
+ """
195
+ return flavor(event), peel(event)
196
+
197
+
198
+ def is_event(msg):
199
+ """
200
+ Return whether the message looks like an event. That is, whether it has a flavor
201
+ that starts with an underscore.
202
+ """
203
+ return flavor(msg).startswith('_')
204
+
205
+
206
+ def origin_identifier(msg):
207
+ """
208
+ Extract the message identifier of a callback query's origin. Returned value
209
+ is guaranteed to be a tuple.
210
+
211
+ ``msg`` is expected to be ``callback_query``.
212
+ """
213
+ if 'message' in msg:
214
+ return msg['message']['chat']['id'], msg['message']['message_id']
215
+ elif 'inline_message_id' in msg:
216
+ return msg['inline_message_id'],
217
+ else:
218
+ raise ValueError()
219
+
220
+ def message_identifier(msg):
221
+ """
222
+ Extract an identifier for message editing. Useful with :meth:`telepot.Bot.editMessageText`
223
+ and similar methods. Returned value is guaranteed to be a tuple.
224
+
225
+ ``msg`` is expected to be ``chat`` or ``choson_inline_result``.
226
+ """
227
+ if 'chat' in msg and 'message_id' in msg:
228
+ return msg['chat']['id'], msg['message_id']
229
+ elif 'inline_message_id' in msg:
230
+ return msg['inline_message_id'],
231
+ else:
232
+ raise ValueError()
233
+
234
+ def _dismantle_message_identifier(f):
235
+ if isinstance(f, tuple):
236
+ if len(f) == 2:
237
+ return {'chat_id': f[0], 'message_id': f[1]}
238
+ elif len(f) == 1:
239
+ return {'inline_message_id': f[0]}
240
+ else:
241
+ raise ValueError()
242
+ else:
243
+ return {'inline_message_id': f}
244
+
245
+ def _split_input_media_array(media_array):
246
+ def ensure_dict(input_media):
247
+ if isinstance(input_media, tuple) and hasattr(input_media, '_asdict'):
248
+ return input_media._asdict()
249
+ elif isinstance(input_media, dict):
250
+ return input_media
251
+ else:
252
+ raise ValueError()
253
+
254
+ def given_attach_name(input_media):
255
+ if isinstance(input_media['media'], tuple):
256
+ return input_media['media'][0]
257
+ else:
258
+ return None
259
+
260
+ def attach_name_generator(used_names):
261
+ x = 0
262
+ while 1:
263
+ x += 1
264
+ name = 'media' + str(x)
265
+ if name in used_names:
266
+ continue;
267
+ yield name
268
+
269
+ def split_media(input_media, name_generator):
270
+ file_spec = input_media['media']
271
+
272
+ # file_id, URL
273
+ if _isstring(file_spec):
274
+ return (input_media, None)
275
+
276
+ # file-object
277
+ # (attach-name, file-object)
278
+ # (attach-name, (filename, file-object))
279
+ if isinstance(file_spec, tuple):
280
+ name, f = file_spec
281
+ else:
282
+ name, f = next(name_generator), file_spec
283
+
284
+ m = input_media.copy()
285
+ m['media'] = 'attach://' + name
286
+
287
+ return (m, (name, f))
288
+
289
+ ms = [ensure_dict(m) for m in media_array]
290
+
291
+ used_names = [given_attach_name(m) for m in ms if given_attach_name(m) is not None]
292
+ name_generator = attach_name_generator(used_names)
293
+
294
+ splitted = [split_media(m, name_generator) for m in ms]
295
+
296
+ legal_media, attachments = map(list, zip(*splitted))
297
+ files_to_attach = dict([a for a in attachments if a is not None])
298
+
299
+ return (legal_media, files_to_attach)
300
+
301
+
302
+ PY_3 = sys.version_info.major >= 3
303
+ _string_type = str if PY_3 else basestring
304
+ _file_type = io.IOBase if PY_3 else file
305
+
306
+ def _isstring(s):
307
+ return isinstance(s, _string_type)
308
+
309
+ def _isfile(f):
310
+ return isinstance(f, _file_type)
311
+
312
+
313
+ from . import helper
314
+
315
+ def flavor_router(routing_table):
316
+ router = helper.Router(flavor, routing_table)
317
+ return router.route
318
+
319
+
320
+ class _BotBase(object):
321
+ def __init__(self, token):
322
+ self._token = token
323
+ self._file_chunk_size = 65536
324
+
325
+
326
+ def _strip(params, more=[]):
327
+ return {key: value for key,value in params.items() if key not in ['self']+more}
328
+
329
+ def _rectify(params):
330
+ def make_jsonable(value):
331
+ if isinstance(value, list):
332
+ return [make_jsonable(v) for v in value]
333
+ elif isinstance(value, dict):
334
+ return {k:make_jsonable(v) for k,v in value.items() if v is not None}
335
+ elif isinstance(value, tuple) and hasattr(value, '_asdict'):
336
+ return {k:make_jsonable(v) for k,v in value._asdict().items() if v is not None}
337
+ else:
338
+ return value
339
+
340
+ def flatten(value):
341
+ v = make_jsonable(value)
342
+
343
+ if isinstance(v, (dict, list)):
344
+ return json.dumps(v, separators=(',',':'))
345
+ else:
346
+ return v
347
+
348
+ # remove None, then json-serialize if needed
349
+ return {k: flatten(v) for k,v in params.items() if v is not None}
350
+
351
+
352
+ from . import api
353
+
354
+ class Bot(_BotBase):
355
+ class Scheduler(threading.Thread):
356
+ # A class that is sorted by timestamp. Use `bisect` module to ensure order in event queue.
357
+ Event = collections.namedtuple('Event', ['timestamp', 'data'])
358
+ Event.__eq__ = lambda self, other: self.timestamp == other.timestamp
359
+ Event.__ne__ = lambda self, other: self.timestamp != other.timestamp
360
+ Event.__gt__ = lambda self, other: self.timestamp > other.timestamp
361
+ Event.__ge__ = lambda self, other: self.timestamp >= other.timestamp
362
+ Event.__lt__ = lambda self, other: self.timestamp < other.timestamp
363
+ Event.__le__ = lambda self, other: self.timestamp <= other.timestamp
364
+
365
+ def __init__(self):
366
+ super(Bot.Scheduler, self).__init__()
367
+ self._eventq = []
368
+ self._lock = threading.RLock() # reentrant lock to allow locked method calling locked method
369
+ self._event_handler = None
370
+
371
+ def _locked(fn):
372
+ def k(self, *args, **kwargs):
373
+ with self._lock:
374
+ return fn(self, *args, **kwargs)
375
+ return k
376
+
377
+ @_locked
378
+ def _insert_event(self, data, when):
379
+ ev = self.Event(when, data)
380
+ bisect.insort(self._eventq, ev)
381
+ return ev
382
+
383
+ @_locked
384
+ def _remove_event(self, event):
385
+ # Find event according to its timestamp.
386
+ # Index returned should be one behind.
387
+ i = bisect.bisect(self._eventq, event)
388
+
389
+ # Having two events with identical timestamp is unlikely but possible.
390
+ # I am going to move forward and compare timestamp AND object address
391
+ # to make sure the correct object is found.
392
+
393
+ while i > 0:
394
+ i -= 1
395
+ e = self._eventq[i]
396
+
397
+ if e.timestamp != event.timestamp:
398
+ raise exception.EventNotFound(event)
399
+ elif id(e) == id(event):
400
+ self._eventq.pop(i)
401
+ return
402
+
403
+ raise exception.EventNotFound(event)
404
+
405
+ @_locked
406
+ def _pop_expired_event(self):
407
+ if not self._eventq:
408
+ return None
409
+
410
+ if self._eventq[0].timestamp <= time.time():
411
+ return self._eventq.pop(0)
412
+ else:
413
+ return None
414
+
415
+ def event_at(self, when, data):
416
+ """
417
+ Schedule some data to emit at an absolute timestamp.
418
+
419
+ :type when: int or float
420
+ :type data: dictionary
421
+ :return: an internal Event object
422
+ """
423
+ return self._insert_event(data, when)
424
+
425
+ def event_later(self, delay, data):
426
+ """
427
+ Schedule some data to emit after a number of seconds.
428
+
429
+ :type delay: int or float
430
+ :type data: dictionary
431
+ :return: an internal Event object
432
+ """
433
+ return self._insert_event(data, time.time()+delay)
434
+
435
+ def event_now(self, data):
436
+ """
437
+ Emit some data as soon as possible.
438
+
439
+ :type data: dictionary
440
+ :return: an internal Event object
441
+ """
442
+ return self._insert_event(data, time.time())
443
+
444
+ def cancel(self, event):
445
+ """
446
+ Cancel an event.
447
+
448
+ :type event: an internal Event object
449
+ """
450
+ self._remove_event(event)
451
+
452
+ def run(self):
453
+ while 1:
454
+ e = self._pop_expired_event()
455
+ while e:
456
+ if callable(e.data):
457
+ d = e.data() # call the data-producing function
458
+ if d is not None:
459
+ self._event_handler(d)
460
+ else:
461
+ self._event_handler(e.data)
462
+
463
+ e = self._pop_expired_event()
464
+ time.sleep(0.1)
465
+
466
+ def run_as_thread(self):
467
+ self.daemon = True
468
+ self.start()
469
+
470
+ def on_event(self, fn):
471
+ self._event_handler = fn
472
+
473
+ def __init__(self, token):
474
+ super(Bot, self).__init__(token)
475
+
476
+ self._scheduler = self.Scheduler()
477
+
478
+ self._router = helper.Router(flavor, {'chat': lambda msg: self.on_chat_message(msg),
479
+ 'callback_query': lambda msg: self.on_callback_query(msg),
480
+ 'inline_query': lambda msg: self.on_inline_query(msg),
481
+ 'chosen_inline_result': lambda msg: self.on_chosen_inline_result(msg)})
482
+ # use lambda to delay evaluation of self.on_ZZZ to runtime because
483
+ # I don't want to require defining all methods right here.
484
+
485
+ @property
486
+ def scheduler(self):
487
+ return self._scheduler
488
+
489
+ @property
490
+ def router(self):
491
+ return self._router
492
+
493
+ def handle(self, msg):
494
+ self._router.route(msg)
495
+
496
+ def _api_request(self, method, params=None, files=None, **kwargs):
497
+ return api.request((self._token, method, params, files), **kwargs)
498
+
499
+ def _api_request_with_file(self, method, params, file_key, file_value, **kwargs):
500
+ if _isstring(file_value):
501
+ params[file_key] = file_value
502
+ return self._api_request(method, _rectify(params), **kwargs)
503
+ else:
504
+ files = {file_key: file_value}
505
+ return self._api_request(method, _rectify(params), files, **kwargs)
506
+
507
+ def getMe(self):
508
+ """ See: https://core.telegram.org/bots/api#getme """
509
+ return self._api_request('getMe')
510
+
511
+ def sendMessage(self, chat_id, text,
512
+ parse_mode=None,
513
+ disable_web_page_preview=None,
514
+ disable_notification=None,
515
+ reply_to_message_id=None,
516
+ reply_markup=None):
517
+ """ See: https://core.telegram.org/bots/api#sendmessage """
518
+ p = _strip(locals())
519
+ return self._api_request('sendMessage', _rectify(p))
520
+
521
+ def forwardMessage(self, chat_id, from_chat_id, message_id,
522
+ disable_notification=None):
523
+ """ See: https://core.telegram.org/bots/api#forwardmessage """
524
+ p = _strip(locals())
525
+ return self._api_request('forwardMessage', _rectify(p))
526
+
527
+ def sendPhoto(self, chat_id, photo,
528
+ caption=None,
529
+ parse_mode=None,
530
+ disable_notification=None,
531
+ reply_to_message_id=None,
532
+ reply_markup=None):
533
+ """
534
+ See: https://core.telegram.org/bots/api#sendphoto
535
+
536
+ :param photo:
537
+ - string: ``file_id`` for a photo existing on Telegram servers
538
+ - string: HTTP URL of a photo from the Internet
539
+ - file-like object: obtained by ``open(path, 'rb')``
540
+ - tuple: (filename, file-like object). If the filename contains
541
+ non-ASCII characters and you are using Python 2.7, make sure the
542
+ filename is a unicode string.
543
+ """
544
+ p = _strip(locals(), more=['photo'])
545
+ return self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo)
546
+
547
+ def sendAudio(self, chat_id, audio,
548
+ caption=None,
549
+ parse_mode=None,
550
+ duration=None,
551
+ performer=None,
552
+ title=None,
553
+ disable_notification=None,
554
+ reply_to_message_id=None,
555
+ reply_markup=None):
556
+ """
557
+ See: https://core.telegram.org/bots/api#sendaudio
558
+
559
+ :param audio: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
560
+ """
561
+ p = _strip(locals(), more=['audio'])
562
+ return self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio)
563
+
564
+ def sendDocument(self, chat_id, document,
565
+ caption=None,
566
+ parse_mode=None,
567
+ disable_notification=None,
568
+ reply_to_message_id=None,
569
+ reply_markup=None):
570
+ """
571
+ See: https://core.telegram.org/bots/api#senddocument
572
+
573
+ :param document: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
574
+ """
575
+ p = _strip(locals(), more=['document'])
576
+ return self._api_request_with_file('sendDocument', _rectify(p), 'document', document)
577
+
578
+ def sendVideo(self, chat_id, video,
579
+ duration=None,
580
+ width=None,
581
+ height=None,
582
+ caption=None,
583
+ parse_mode=None,
584
+ supports_streaming=None,
585
+ disable_notification=None,
586
+ reply_to_message_id=None,
587
+ reply_markup=None):
588
+ """
589
+ See: https://core.telegram.org/bots/api#sendvideo
590
+
591
+ :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
592
+ """
593
+ p = _strip(locals(), more=['video'])
594
+ return self._api_request_with_file('sendVideo', _rectify(p), 'video', video)
595
+
596
+ def sendVoice(self, chat_id, voice,
597
+ caption=None,
598
+ parse_mode=None,
599
+ duration=None,
600
+ disable_notification=None,
601
+ reply_to_message_id=None,
602
+ reply_markup=None):
603
+ """
604
+ See: https://core.telegram.org/bots/api#sendvoice
605
+
606
+ :param voice: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
607
+ """
608
+ p = _strip(locals(), more=['voice'])
609
+ return self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice)
610
+
611
+ def sendVideoNote(self, chat_id, video_note,
612
+ duration=None,
613
+ length=None,
614
+ disable_notification=None,
615
+ reply_to_message_id=None,
616
+ reply_markup=None):
617
+ """
618
+ See: https://core.telegram.org/bots/api#sendvideonote
619
+
620
+ :param video_note: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
621
+
622
+ :param length:
623
+ Although marked as optional, this method does not seem to work without
624
+ it being specified. Supply any integer you want. It seems to have no effect
625
+ on the video note's display size.
626
+ """
627
+ p = _strip(locals(), more=['video_note'])
628
+ return self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note)
629
+
630
+ def sendMediaGroup(self, chat_id, media,
631
+ disable_notification=None,
632
+ reply_to_message_id=None):
633
+ """
634
+ See: https://core.telegram.org/bots/api#sendmediagroup
635
+
636
+ :type media: array of `InputMedia <https://core.telegram.org/bots/api#inputmedia>`_ objects
637
+ :param media:
638
+ To indicate media locations, each InputMedia object's ``media`` field
639
+ should be one of these:
640
+
641
+ - string: ``file_id`` for a file existing on Telegram servers
642
+ - string: HTTP URL of a file from the Internet
643
+ - file-like object: obtained by ``open(path, 'rb')``
644
+ - tuple: (form-data name, file-like object)
645
+ - tuple: (form-data name, (filename, file-like object))
646
+
647
+ In case of uploading, you may supply customized multipart/form-data
648
+ names for each uploaded file (as in last 2 options above). Otherwise,
649
+ telepot assigns unique names to each uploaded file. Names assigned by
650
+ telepot will not collide with user-supplied names, if any.
651
+ """
652
+ p = _strip(locals(), more=['media'])
653
+ legal_media, files_to_attach = _split_input_media_array(media)
654
+
655
+ p['media'] = legal_media
656
+ return self._api_request('sendMediaGroup', _rectify(p), files_to_attach)
657
+
658
+ def sendLocation(self, chat_id, latitude, longitude,
659
+ live_period=None,
660
+ disable_notification=None,
661
+ reply_to_message_id=None,
662
+ reply_markup=None):
663
+ """ See: https://core.telegram.org/bots/api#sendlocation """
664
+ p = _strip(locals())
665
+ return self._api_request('sendLocation', _rectify(p))
666
+
667
+ def editMessageLiveLocation(self, msg_identifier, latitude, longitude,
668
+ reply_markup=None):
669
+ """
670
+ See: https://core.telegram.org/bots/api#editmessagelivelocation
671
+
672
+ :param msg_identifier: Same as in :meth:`.Bot.editMessageText`
673
+ """
674
+ p = _strip(locals(), more=['msg_identifier'])
675
+ p.update(_dismantle_message_identifier(msg_identifier))
676
+ return self._api_request('editMessageLiveLocation', _rectify(p))
677
+
678
+ def stopMessageLiveLocation(self, msg_identifier,
679
+ reply_markup=None):
680
+ """
681
+ See: https://core.telegram.org/bots/api#stopmessagelivelocation
682
+
683
+ :param msg_identifier: Same as in :meth:`.Bot.editMessageText`
684
+ """
685
+ p = _strip(locals(), more=['msg_identifier'])
686
+ p.update(_dismantle_message_identifier(msg_identifier))
687
+ return self._api_request('stopMessageLiveLocation', _rectify(p))
688
+
689
+ def sendVenue(self, chat_id, latitude, longitude, title, address,
690
+ foursquare_id=None,
691
+ disable_notification=None,
692
+ reply_to_message_id=None,
693
+ reply_markup=None):
694
+ """ See: https://core.telegram.org/bots/api#sendvenue """
695
+ p = _strip(locals())
696
+ return self._api_request('sendVenue', _rectify(p))
697
+
698
+ def sendContact(self, chat_id, phone_number, first_name,
699
+ last_name=None,
700
+ disable_notification=None,
701
+ reply_to_message_id=None,
702
+ reply_markup=None):
703
+ """ See: https://core.telegram.org/bots/api#sendcontact """
704
+ p = _strip(locals())
705
+ return self._api_request('sendContact', _rectify(p))
706
+
707
+ def sendGame(self, chat_id, game_short_name,
708
+ disable_notification=None,
709
+ reply_to_message_id=None,
710
+ reply_markup=None):
711
+ """ See: https://core.telegram.org/bots/api#sendgame """
712
+ p = _strip(locals())
713
+ return self._api_request('sendGame', _rectify(p))
714
+
715
+ def sendInvoice(self, chat_id, title, description, payload,
716
+ provider_token, start_parameter, currency, prices,
717
+ provider_data=None,
718
+ photo_url=None,
719
+ photo_size=None,
720
+ photo_width=None,
721
+ photo_height=None,
722
+ need_name=None,
723
+ need_phone_number=None,
724
+ need_email=None,
725
+ need_shipping_address=None,
726
+ is_flexible=None,
727
+ disable_notification=None,
728
+ reply_to_message_id=None,
729
+ reply_markup=None):
730
+ """ See: https://core.telegram.org/bots/api#sendinvoice """
731
+ p = _strip(locals())
732
+ return self._api_request('sendInvoice', _rectify(p))
733
+
734
+ def sendChatAction(self, chat_id, action):
735
+ """ See: https://core.telegram.org/bots/api#sendchataction """
736
+ p = _strip(locals())
737
+ return self._api_request('sendChatAction', _rectify(p))
738
+
739
+ def getUserProfilePhotos(self, user_id,
740
+ offset=None,
741
+ limit=None):
742
+ """ See: https://core.telegram.org/bots/api#getuserprofilephotos """
743
+ p = _strip(locals())
744
+ return self._api_request('getUserProfilePhotos', _rectify(p))
745
+
746
+ def getFile(self, file_id):
747
+ """ See: https://core.telegram.org/bots/api#getfile """
748
+ p = _strip(locals())
749
+ return self._api_request('getFile', _rectify(p))
750
+
751
+ def kickChatMember(self, chat_id, user_id,
752
+ until_date=None):
753
+ """ See: https://core.telegram.org/bots/api#kickchatmember """
754
+ p = _strip(locals())
755
+ return self._api_request('kickChatMember', _rectify(p))
756
+
757
+ def unbanChatMember(self, chat_id, user_id):
758
+ """ See: https://core.telegram.org/bots/api#unbanchatmember """
759
+ p = _strip(locals())
760
+ return self._api_request('unbanChatMember', _rectify(p))
761
+
762
+ def restrictChatMember(self, chat_id, user_id,
763
+ until_date=None,
764
+ can_send_messages=None,
765
+ can_send_media_messages=None,
766
+ can_send_other_messages=None,
767
+ can_add_web_page_previews=None):
768
+ """ See: https://core.telegram.org/bots/api#restrictchatmember """
769
+ p = _strip(locals())
770
+ return self._api_request('restrictChatMember', _rectify(p))
771
+
772
+ def promoteChatMember(self, chat_id, user_id,
773
+ can_change_info=None,
774
+ can_post_messages=None,
775
+ can_edit_messages=None,
776
+ can_delete_messages=None,
777
+ can_invite_users=None,
778
+ can_restrict_members=None,
779
+ can_pin_messages=None,
780
+ can_promote_members=None):
781
+ """ See: https://core.telegram.org/bots/api#promotechatmember """
782
+ p = _strip(locals())
783
+ return self._api_request('promoteChatMember', _rectify(p))
784
+
785
+ def exportChatInviteLink(self, chat_id):
786
+ """ See: https://core.telegram.org/bots/api#exportchatinvitelink """
787
+ p = _strip(locals())
788
+ return self._api_request('exportChatInviteLink', _rectify(p))
789
+
790
+ def setChatPhoto(self, chat_id, photo):
791
+ """ See: https://core.telegram.org/bots/api#setchatphoto """
792
+ p = _strip(locals(), more=['photo'])
793
+ return self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo)
794
+
795
+ def deleteChatPhoto(self, chat_id):
796
+ """ See: https://core.telegram.org/bots/api#deletechatphoto """
797
+ p = _strip(locals())
798
+ return self._api_request('deleteChatPhoto', _rectify(p))
799
+
800
+ def setChatTitle(self, chat_id, title):
801
+ """ See: https://core.telegram.org/bots/api#setchattitle """
802
+ p = _strip(locals())
803
+ return self._api_request('setChatTitle', _rectify(p))
804
+
805
+ def setChatDescription(self, chat_id,
806
+ description=None):
807
+ """ See: https://core.telegram.org/bots/api#setchatdescription """
808
+ p = _strip(locals())
809
+ return self._api_request('setChatDescription', _rectify(p))
810
+
811
+ def pinChatMessage(self, chat_id, message_id,
812
+ disable_notification=None):
813
+ """ See: https://core.telegram.org/bots/api#pinchatmessage """
814
+ p = _strip(locals())
815
+ return self._api_request('pinChatMessage', _rectify(p))
816
+
817
+ def unpinChatMessage(self, chat_id):
818
+ """ See: https://core.telegram.org/bots/api#unpinchatmessage """
819
+ p = _strip(locals())
820
+ return self._api_request('unpinChatMessage', _rectify(p))
821
+
822
+ def leaveChat(self, chat_id):
823
+ """ See: https://core.telegram.org/bots/api#leavechat """
824
+ p = _strip(locals())
825
+ return self._api_request('leaveChat', _rectify(p))
826
+
827
+ def getChat(self, chat_id):
828
+ """ See: https://core.telegram.org/bots/api#getchat """
829
+ p = _strip(locals())
830
+ return self._api_request('getChat', _rectify(p))
831
+
832
+ def getChatAdministrators(self, chat_id):
833
+ """ See: https://core.telegram.org/bots/api#getchatadministrators """
834
+ p = _strip(locals())
835
+ return self._api_request('getChatAdministrators', _rectify(p))
836
+
837
+ def getChatMembersCount(self, chat_id):
838
+ """ See: https://core.telegram.org/bots/api#getchatmemberscount """
839
+ p = _strip(locals())
840
+ return self._api_request('getChatMembersCount', _rectify(p))
841
+
842
+ def getChatMember(self, chat_id, user_id):
843
+ """ See: https://core.telegram.org/bots/api#getchatmember """
844
+ p = _strip(locals())
845
+ return self._api_request('getChatMember', _rectify(p))
846
+
847
+ def setChatStickerSet(self, chat_id, sticker_set_name):
848
+ """ See: https://core.telegram.org/bots/api#setchatstickerset """
849
+ p = _strip(locals())
850
+ return self._api_request('setChatStickerSet', _rectify(p))
851
+
852
+ def deleteChatStickerSet(self, chat_id):
853
+ """ See: https://core.telegram.org/bots/api#deletechatstickerset """
854
+ p = _strip(locals())
855
+ return self._api_request('deleteChatStickerSet', _rectify(p))
856
+
857
+ def answerCallbackQuery(self, callback_query_id,
858
+ text=None,
859
+ show_alert=None,
860
+ url=None,
861
+ cache_time=None):
862
+ """ See: https://core.telegram.org/bots/api#answercallbackquery """
863
+ p = _strip(locals())
864
+ return self._api_request('answerCallbackQuery', _rectify(p))
865
+
866
+ def answerShippingQuery(self, shipping_query_id, ok,
867
+ shipping_options=None,
868
+ error_message=None):
869
+ """ See: https://core.telegram.org/bots/api#answershippingquery """
870
+ p = _strip(locals())
871
+ return self._api_request('answerShippingQuery', _rectify(p))
872
+
873
+ def answerPreCheckoutQuery(self, pre_checkout_query_id, ok,
874
+ error_message=None):
875
+ """ See: https://core.telegram.org/bots/api#answerprecheckoutquery """
876
+ p = _strip(locals())
877
+ return self._api_request('answerPreCheckoutQuery', _rectify(p))
878
+
879
+ def editMessageText(self, msg_identifier, text,
880
+ parse_mode=None,
881
+ disable_web_page_preview=None,
882
+ reply_markup=None):
883
+ """
884
+ See: https://core.telegram.org/bots/api#editmessagetext
885
+
886
+ :param msg_identifier:
887
+ a 2-tuple (``chat_id``, ``message_id``),
888
+ a 1-tuple (``inline_message_id``),
889
+ or simply ``inline_message_id``.
890
+ You may extract this value easily with :meth:`telepot.message_identifier`
891
+ """
892
+ p = _strip(locals(), more=['msg_identifier'])
893
+ p.update(_dismantle_message_identifier(msg_identifier))
894
+ return self._api_request('editMessageText', _rectify(p))
895
+
896
+ def editMessageCaption(self, msg_identifier,
897
+ caption=None,
898
+ parse_mode=None,
899
+ reply_markup=None):
900
+ """
901
+ See: https://core.telegram.org/bots/api#editmessagecaption
902
+
903
+ :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`
904
+ """
905
+ p = _strip(locals(), more=['msg_identifier'])
906
+ p.update(_dismantle_message_identifier(msg_identifier))
907
+ return self._api_request('editMessageCaption', _rectify(p))
908
+
909
+ def editMessageReplyMarkup(self, msg_identifier,
910
+ reply_markup=None):
911
+ """
912
+ See: https://core.telegram.org/bots/api#editmessagereplymarkup
913
+
914
+ :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`
915
+ """
916
+ p = _strip(locals(), more=['msg_identifier'])
917
+ p.update(_dismantle_message_identifier(msg_identifier))
918
+ return self._api_request('editMessageReplyMarkup', _rectify(p))
919
+
920
+ def deleteMessage(self, msg_identifier):
921
+ """
922
+ See: https://core.telegram.org/bots/api#deletemessage
923
+
924
+ :param msg_identifier:
925
+ Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`,
926
+ except this method does not work on inline messages.
927
+ """
928
+ p = _strip(locals(), more=['msg_identifier'])
929
+ p.update(_dismantle_message_identifier(msg_identifier))
930
+ return self._api_request('deleteMessage', _rectify(p))
931
+
932
+ def sendSticker(self, chat_id, sticker,
933
+ disable_notification=None,
934
+ reply_to_message_id=None,
935
+ reply_markup=None):
936
+ """
937
+ See: https://core.telegram.org/bots/api#sendsticker
938
+
939
+ :param sticker: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto`
940
+ """
941
+ p = _strip(locals(), more=['sticker'])
942
+ return self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker)
943
+
944
+ def getStickerSet(self, name):
945
+ """
946
+ See: https://core.telegram.org/bots/api#getstickerset
947
+ """
948
+ p = _strip(locals())
949
+ return self._api_request('getStickerSet', _rectify(p))
950
+
951
+ def uploadStickerFile(self, user_id, png_sticker):
952
+ """
953
+ See: https://core.telegram.org/bots/api#uploadstickerfile
954
+ """
955
+ p = _strip(locals(), more=['png_sticker'])
956
+ return self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker)
957
+
958
+ def createNewStickerSet(self, user_id, name, title, png_sticker, emojis,
959
+ contains_masks=None,
960
+ mask_position=None):
961
+ """
962
+ See: https://core.telegram.org/bots/api#createnewstickerset
963
+ """
964
+ p = _strip(locals(), more=['png_sticker'])
965
+ return self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker)
966
+
967
+ def addStickerToSet(self, user_id, name, png_sticker, emojis,
968
+ mask_position=None):
969
+ """
970
+ See: https://core.telegram.org/bots/api#addstickertoset
971
+ """
972
+ p = _strip(locals(), more=['png_sticker'])
973
+ return self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker)
974
+
975
+ def setStickerPositionInSet(self, sticker, position):
976
+ """
977
+ See: https://core.telegram.org/bots/api#setstickerpositioninset
978
+ """
979
+ p = _strip(locals())
980
+ return self._api_request('setStickerPositionInSet', _rectify(p))
981
+
982
+ def deleteStickerFromSet(self, sticker):
983
+ """
984
+ See: https://core.telegram.org/bots/api#deletestickerfromset
985
+ """
986
+ p = _strip(locals())
987
+ return self._api_request('deleteStickerFromSet', _rectify(p))
988
+
989
+ def answerInlineQuery(self, inline_query_id, results,
990
+ cache_time=None,
991
+ is_personal=None,
992
+ next_offset=None,
993
+ switch_pm_text=None,
994
+ switch_pm_parameter=None):
995
+ """ See: https://core.telegram.org/bots/api#answerinlinequery """
996
+ p = _strip(locals())
997
+ return self._api_request('answerInlineQuery', _rectify(p))
998
+
999
+ def getUpdates(self,
1000
+ offset=None,
1001
+ limit=None,
1002
+ timeout=None,
1003
+ allowed_updates=None):
1004
+ """ See: https://core.telegram.org/bots/api#getupdates """
1005
+ p = _strip(locals())
1006
+ return self._api_request('getUpdates', _rectify(p))
1007
+
1008
+ def setWebhook(self,
1009
+ url=None,
1010
+ certificate=None,
1011
+ max_connections=None,
1012
+ allowed_updates=None):
1013
+ """ See: https://core.telegram.org/bots/api#setwebhook """
1014
+ p = _strip(locals(), more=['certificate'])
1015
+
1016
+ if certificate:
1017
+ files = {'certificate': certificate}
1018
+ return self._api_request('setWebhook', _rectify(p), files)
1019
+ else:
1020
+ return self._api_request('setWebhook', _rectify(p))
1021
+
1022
+ def deleteWebhook(self):
1023
+ """ See: https://core.telegram.org/bots/api#deletewebhook """
1024
+ return self._api_request('deleteWebhook')
1025
+
1026
+ def getWebhookInfo(self):
1027
+ """ See: https://core.telegram.org/bots/api#getwebhookinfo """
1028
+ return self._api_request('getWebhookInfo')
1029
+
1030
+ def setGameScore(self, user_id, score, game_message_identifier,
1031
+ force=None,
1032
+ disable_edit_message=None):
1033
+ """
1034
+ See: https://core.telegram.org/bots/api#setgamescore
1035
+
1036
+ :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`
1037
+ """
1038
+ p = _strip(locals(), more=['game_message_identifier'])
1039
+ p.update(_dismantle_message_identifier(game_message_identifier))
1040
+ return self._api_request('setGameScore', _rectify(p))
1041
+
1042
+ def getGameHighScores(self, user_id, game_message_identifier):
1043
+ """
1044
+ See: https://core.telegram.org/bots/api#getgamehighscores
1045
+
1046
+ :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`
1047
+ """
1048
+ p = _strip(locals(), more=['game_message_identifier'])
1049
+ p.update(_dismantle_message_identifier(game_message_identifier))
1050
+ return self._api_request('getGameHighScores', _rectify(p))
1051
+
1052
+ def download_file(self, file_id, dest):
1053
+ """
1054
+ Download a file to local disk.
1055
+
1056
+ :param dest: a path or a ``file`` object
1057
+ """
1058
+ f = self.getFile(file_id)
1059
+ try:
1060
+ d = dest if _isfile(dest) else open(dest, 'wb')
1061
+
1062
+ r = api.download((self._token, f['file_path']), preload_content=False)
1063
+
1064
+ while 1:
1065
+ data = r.read(self._file_chunk_size)
1066
+ if not data:
1067
+ break
1068
+ d.write(data)
1069
+ finally:
1070
+ if not _isfile(dest) and 'd' in locals():
1071
+ d.close()
1072
+
1073
+ if 'r' in locals():
1074
+ r.release_conn()
1075
+
1076
+ def message_loop(self, callback=None, relax=0.1,
1077
+ timeout=20, allowed_updates=None,
1078
+ source=None, ordered=True, maxhold=3,
1079
+ run_forever=False):
1080
+ """
1081
+ :deprecated: will be removed in future. Use :class:`.MessageLoop` instead.
1082
+
1083
+ Spawn a thread to constantly ``getUpdates`` or pull updates from a queue.
1084
+ Apply ``callback`` to every message received. Also starts the scheduler thread
1085
+ for internal events.
1086
+
1087
+ :param callback:
1088
+ a function that takes one argument (the message), or a routing table.
1089
+ If ``None``, the bot's ``handle`` method is used.
1090
+
1091
+ A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate
1092
+ handler functions according to their flavors. It allows you to define functions specifically
1093
+ to handle one flavor of messages. It usually looks like this: ``{'chat': fn1,
1094
+ 'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take
1095
+ one argument (the message).
1096
+
1097
+ :param source:
1098
+ Source of updates.
1099
+ If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers.
1100
+ If it is a synchronized queue (``Queue.Queue`` in Python 2.7 or
1101
+ ``queue.Queue`` in Python 3), new messages are pulled from the queue.
1102
+ A web application implementing a webhook can dump updates into the queue,
1103
+ while the bot pulls from it. This is how telepot can be integrated with webhooks.
1104
+
1105
+ Acceptable contents in queue:
1106
+
1107
+ - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8)
1108
+ representing a JSON-serialized `Update <https://core.telegram.org/bots/api#update>`_ object.
1109
+ - a ``dict`` representing an Update object.
1110
+
1111
+ When ``source`` is ``None``, these parameters are meaningful:
1112
+
1113
+ :type relax: float
1114
+ :param relax: seconds between each ``getUpdates``
1115
+
1116
+ :type timeout: int
1117
+ :param timeout:
1118
+ ``timeout`` parameter supplied to :meth:`telepot.Bot.getUpdates`,
1119
+ controlling how long to poll.
1120
+
1121
+ :type allowed_updates: array of string
1122
+ :param allowed_updates:
1123
+ ``allowed_updates`` parameter supplied to :meth:`telepot.Bot.getUpdates`,
1124
+ controlling which types of updates to receive.
1125
+
1126
+ When ``source`` is a queue, these parameters are meaningful:
1127
+
1128
+ :type ordered: bool
1129
+ :param ordered:
1130
+ If ``True``, ensure in-order delivery of messages to ``callback``
1131
+ (i.e. updates with a smaller ``update_id`` always come before those with
1132
+ a larger ``update_id``).
1133
+ If ``False``, no re-ordering is done. ``callback`` is applied to messages
1134
+ as soon as they are pulled from queue.
1135
+
1136
+ :type maxhold: float
1137
+ :param maxhold:
1138
+ Applied only when ``ordered`` is ``True``. The maximum number of seconds
1139
+ an update is held waiting for a not-yet-arrived smaller ``update_id``.
1140
+ When this number of seconds is up, the update is delivered to ``callback``
1141
+ even if some smaller ``update_id``\s have not yet arrived. If those smaller
1142
+ ``update_id``\s arrive at some later time, they are discarded.
1143
+
1144
+ Finally, there is this parameter, meaningful always:
1145
+
1146
+ :type run_forever: bool or str
1147
+ :param run_forever:
1148
+ If ``True`` or any non-empty string, append an infinite loop at the end of
1149
+ this method, so it never returns. Useful as the very last line in a program.
1150
+ A non-empty string will also be printed, useful as an indication that the
1151
+ program is listening.
1152
+ """
1153
+ if callback is None:
1154
+ callback = self.handle
1155
+ elif isinstance(callback, dict):
1156
+ callback = flavor_router(callback)
1157
+
1158
+ collect_queue = queue.Queue()
1159
+
1160
+ def collector():
1161
+ while 1:
1162
+ try:
1163
+ item = collect_queue.get(block=True)
1164
+ callback(item)
1165
+ except:
1166
+ # Localize error so thread can keep going.
1167
+ traceback.print_exc()
1168
+
1169
+ def relay_to_collector(update):
1170
+ key = _find_first_key(update, ['message',
1171
+ 'edited_message',
1172
+ 'channel_post',
1173
+ 'edited_channel_post',
1174
+ 'callback_query',
1175
+ 'inline_query',
1176
+ 'chosen_inline_result',
1177
+ 'shipping_query',
1178
+ 'pre_checkout_query'])
1179
+ collect_queue.put(update[key])
1180
+ return update['update_id']
1181
+
1182
+ def get_from_telegram_server():
1183
+ offset = None # running offset
1184
+ allowed_upd = allowed_updates
1185
+ while 1:
1186
+ try:
1187
+ result = self.getUpdates(offset=offset,
1188
+ timeout=timeout,
1189
+ allowed_updates=allowed_upd)
1190
+
1191
+ # Once passed, this parameter is no longer needed.
1192
+ allowed_upd = None
1193
+
1194
+ if len(result) > 0:
1195
+ # No sort. Trust server to give messages in correct order.
1196
+ # Update offset to max(update_id) + 1
1197
+ offset = max([relay_to_collector(update) for update in result]) + 1
1198
+
1199
+ except exception.BadHTTPResponse as e:
1200
+ traceback.print_exc()
1201
+
1202
+ # Servers probably down. Wait longer.
1203
+ if e.status == 502:
1204
+ time.sleep(30)
1205
+ except:
1206
+ traceback.print_exc()
1207
+ finally:
1208
+ time.sleep(relax)
1209
+
1210
+ def dictify3(data):
1211
+ if type(data) is bytes:
1212
+ return json.loads(data.decode('utf-8'))
1213
+ elif type(data) is str:
1214
+ return json.loads(data)
1215
+ elif type(data) is dict:
1216
+ return data
1217
+ else:
1218
+ raise ValueError()
1219
+
1220
+ def dictify27(data):
1221
+ if type(data) in [str, unicode]:
1222
+ return json.loads(data)
1223
+ elif type(data) is dict:
1224
+ return data
1225
+ else:
1226
+ raise ValueError()
1227
+
1228
+ def get_from_queue_unordered(qu):
1229
+ dictify = dictify3 if sys.version_info >= (3,) else dictify27
1230
+ while 1:
1231
+ try:
1232
+ data = qu.get(block=True)
1233
+ update = dictify(data)
1234
+ relay_to_collector(update)
1235
+ except:
1236
+ traceback.print_exc()
1237
+
1238
+ def get_from_queue(qu):
1239
+ dictify = dictify3 if sys.version_info >= (3,) else dictify27
1240
+
1241
+ # Here is the re-ordering mechanism, ensuring in-order delivery of updates.
1242
+ max_id = None # max update_id passed to callback
1243
+ buffer = collections.deque() # keep those updates which skip some update_id
1244
+ qwait = None # how long to wait for updates,
1245
+ # because buffer's content has to be returned in time.
1246
+
1247
+ while 1:
1248
+ try:
1249
+ data = qu.get(block=True, timeout=qwait)
1250
+ update = dictify(data)
1251
+
1252
+ if max_id is None:
1253
+ # First message received, handle regardless.
1254
+ max_id = relay_to_collector(update)
1255
+
1256
+ elif update['update_id'] == max_id + 1:
1257
+ # No update_id skipped, handle naturally.
1258
+ max_id = relay_to_collector(update)
1259
+
1260
+ # clear contagious updates in buffer
1261
+ if len(buffer) > 0:
1262
+ buffer.popleft() # first element belongs to update just received, useless now.
1263
+ while 1:
1264
+ try:
1265
+ if type(buffer[0]) is dict:
1266
+ max_id = relay_to_collector(buffer.popleft()) # updates that arrived earlier, handle them.
1267
+ else:
1268
+ break # gap, no more contagious updates
1269
+ except IndexError:
1270
+ break # buffer empty
1271
+
1272
+ elif update['update_id'] > max_id + 1:
1273
+ # Update arrives pre-maturely, insert to buffer.
1274
+ nbuf = len(buffer)
1275
+ if update['update_id'] <= max_id + nbuf:
1276
+ # buffer long enough, put update at position
1277
+ buffer[update['update_id'] - max_id - 1] = update
1278
+ else:
1279
+ # buffer too short, lengthen it
1280
+ expire = time.time() + maxhold
1281
+ for a in range(nbuf, update['update_id']-max_id-1):
1282
+ buffer.append(expire) # put expiry time in gaps
1283
+ buffer.append(update)
1284
+
1285
+ else:
1286
+ pass # discard
1287
+
1288
+ except queue.Empty:
1289
+ # debug message
1290
+ # print('Timeout')
1291
+
1292
+ # some buffer contents have to be handled
1293
+ # flush buffer until a non-expired time is encountered
1294
+ while 1:
1295
+ try:
1296
+ if type(buffer[0]) is dict:
1297
+ max_id = relay_to_collector(buffer.popleft())
1298
+ else:
1299
+ expire = buffer[0]
1300
+ if expire <= time.time():
1301
+ max_id += 1
1302
+ buffer.popleft()
1303
+ else:
1304
+ break # non-expired
1305
+ except IndexError:
1306
+ break # buffer empty
1307
+ except:
1308
+ traceback.print_exc()
1309
+ finally:
1310
+ try:
1311
+ # don't wait longer than next expiry time
1312
+ qwait = buffer[0] - time.time()
1313
+ if qwait < 0:
1314
+ qwait = 0
1315
+ except IndexError:
1316
+ # buffer empty, can wait forever
1317
+ qwait = None
1318
+
1319
+ # debug message
1320
+ # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
1321
+
1322
+ collector_thread = threading.Thread(target=collector)
1323
+ collector_thread.daemon = True
1324
+ collector_thread.start()
1325
+
1326
+ if source is None:
1327
+ message_thread = threading.Thread(target=get_from_telegram_server)
1328
+ elif isinstance(source, queue.Queue):
1329
+ if ordered:
1330
+ message_thread = threading.Thread(target=get_from_queue, args=(source,))
1331
+ else:
1332
+ message_thread = threading.Thread(target=get_from_queue_unordered, args=(source,))
1333
+ else:
1334
+ raise ValueError('Invalid source')
1335
+
1336
+ message_thread.daemon = True # need this for main thread to be killable by Ctrl-C
1337
+ message_thread.start()
1338
+
1339
+ self._scheduler.on_event(collect_queue.put)
1340
+ self._scheduler.run_as_thread()
1341
+
1342
+ if run_forever:
1343
+ if _isstring(run_forever):
1344
+ print(run_forever)
1345
+ while 1:
1346
+ time.sleep(10)
1347
+
1348
+
1349
+ import inspect
1350
+
1351
+ class SpeakerBot(Bot):
1352
+ def __init__(self, token):
1353
+ super(SpeakerBot, self).__init__(token)
1354
+ self._mic = helper.Microphone()
1355
+
1356
+ @property
1357
+ def mic(self):
1358
+ return self._mic
1359
+
1360
+ def create_listener(self):
1361
+ q = queue.Queue()
1362
+ self._mic.add(q)
1363
+ ln = helper.Listener(self._mic, q)
1364
+ return ln
1365
+
1366
+
1367
+ class DelegatorBot(SpeakerBot):
1368
+ def __init__(self, token, delegation_patterns):
1369
+ """
1370
+ :param delegation_patterns: a list of (seeder, delegator) tuples.
1371
+ """
1372
+ super(DelegatorBot, self).__init__(token)
1373
+ self._delegate_records = [p+({},) for p in delegation_patterns]
1374
+
1375
+ def _startable(self, delegate):
1376
+ return ((hasattr(delegate, 'start') and inspect.ismethod(delegate.start)) and
1377
+ (hasattr(delegate, 'is_alive') and inspect.ismethod(delegate.is_alive)))
1378
+
1379
+ def _tuple_is_valid(self, t):
1380
+ return len(t) == 3 and callable(t[0]) and type(t[1]) in [list, tuple] and type(t[2]) is dict
1381
+
1382
+ def _ensure_startable(self, delegate):
1383
+ if self._startable(delegate):
1384
+ return delegate
1385
+ elif callable(delegate):
1386
+ return threading.Thread(target=delegate)
1387
+ elif type(delegate) is tuple and self._tuple_is_valid(delegate):
1388
+ func, args, kwargs = delegate
1389
+ return threading.Thread(target=func, args=args, kwargs=kwargs)
1390
+ else:
1391
+ raise RuntimeError('Delegate does not have the required methods, is not callable, and is not a valid tuple.')
1392
+
1393
+ def handle(self, msg):
1394
+ self._mic.send(msg)
1395
+
1396
+ for calculate_seed, make_delegate, dict in self._delegate_records:
1397
+ id = calculate_seed(msg)
1398
+
1399
+ if id is None:
1400
+ continue
1401
+ elif isinstance(id, collections.Hashable):
1402
+ if id not in dict or not dict[id].is_alive():
1403
+ d = make_delegate((self, msg, id))
1404
+ d = self._ensure_startable(d)
1405
+
1406
+ dict[id] = d
1407
+ dict[id].start()
1408
+ else:
1409
+ d = make_delegate((self, msg, id))
1410
+ d = self._ensure_startable(d)
1411
+ d.start()
telepot/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (75.2 kB). View file
 
telepot/__pycache__/api.cpython-311.pyc ADDED
Binary file (8.62 kB). View file
 
telepot/__pycache__/delegate.cpython-311.pyc ADDED
Binary file (20.5 kB). View file
 
telepot/__pycache__/exception.cpython-311.pyc ADDED
Binary file (7.96 kB). View file
 
telepot/__pycache__/filtering.cpython-311.pyc ADDED
Binary file (2.76 kB). View file
 
telepot/__pycache__/hack.cpython-311.pyc ADDED
Binary file (887 Bytes). View file
 
telepot/__pycache__/helper.cpython-311.pyc ADDED
Binary file (64.4 kB). View file
 
telepot/__pycache__/loop.cpython-311.pyc ADDED
Binary file (15.3 kB). View file
 
telepot/__pycache__/namedtuple.cpython-311.pyc ADDED
Binary file (22 kB). View file
 
telepot/__pycache__/routing.cpython-311.pyc ADDED
Binary file (10.9 kB). View file
 
telepot/__pycache__/text.cpython-311.pyc ADDED
Binary file (5.97 kB). View file
 
telepot/aio/__init__.py ADDED
@@ -0,0 +1,926 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ import time
4
+ import asyncio
5
+ import traceback
6
+ import collections
7
+ from concurrent.futures._base import CancelledError
8
+ from . import helper, api
9
+ from .. import (
10
+ _BotBase, flavor, _find_first_key, _isstring, _strip, _rectify,
11
+ _dismantle_message_identifier, _split_input_media_array
12
+ )
13
+
14
+ # Patch aiohttp for sending unicode filename
15
+ from . import hack
16
+
17
+ from .. import exception
18
+
19
+
20
+ def flavor_router(routing_table):
21
+ router = helper.Router(flavor, routing_table)
22
+ return router.route
23
+
24
+
25
+ class Bot(_BotBase):
26
+ class Scheduler(object):
27
+ def __init__(self, loop):
28
+ self._loop = loop
29
+ self._callback = None
30
+
31
+ def on_event(self, callback):
32
+ self._callback = callback
33
+
34
+ def event_at(self, when, data):
35
+ delay = when - time.time()
36
+ return self._loop.call_later(delay, self._callback, data)
37
+ # call_at() uses event loop time, not unix time.
38
+ # May as well use call_later here.
39
+
40
+ def event_later(self, delay, data):
41
+ return self._loop.call_later(delay, self._callback, data)
42
+
43
+ def event_now(self, data):
44
+ return self._loop.call_soon(self._callback, data)
45
+
46
+ def cancel(self, event):
47
+ return event.cancel()
48
+
49
+ def __init__(self, token, loop=None):
50
+ super(Bot, self).__init__(token)
51
+
52
+ self._loop = loop or asyncio.get_event_loop()
53
+ api._loop = self._loop # sync loop with api module
54
+
55
+ self._scheduler = self.Scheduler(self._loop)
56
+
57
+ self._router = helper.Router(flavor, {'chat': helper._create_invoker(self, 'on_chat_message'),
58
+ 'callback_query': helper._create_invoker(self, 'on_callback_query'),
59
+ 'inline_query': helper._create_invoker(self, 'on_inline_query'),
60
+ 'chosen_inline_result': helper._create_invoker(self, 'on_chosen_inline_result')})
61
+
62
+ @property
63
+ def loop(self):
64
+ return self._loop
65
+
66
+ @property
67
+ def scheduler(self):
68
+ return self._scheduler
69
+
70
+ @property
71
+ def router(self):
72
+ return self._router
73
+
74
+ async def handle(self, msg):
75
+ await self._router.route(msg)
76
+
77
+ async def _api_request(self, method, params=None, files=None, **kwargs):
78
+ return await api.request((self._token, method, params, files), **kwargs)
79
+
80
+ async def _api_request_with_file(self, method, params, file_key, file_value, **kwargs):
81
+ if _isstring(file_value):
82
+ params[file_key] = file_value
83
+ return await self._api_request(method, _rectify(params), **kwargs)
84
+ else:
85
+ files = {file_key: file_value}
86
+ return await self._api_request(method, _rectify(params), files, **kwargs)
87
+
88
+ async def getMe(self):
89
+ """ See: https://core.telegram.org/bots/api#getme """
90
+ return await self._api_request('getMe')
91
+
92
+ async def sendMessage(self, chat_id, text,
93
+ parse_mode=None,
94
+ disable_web_page_preview=None,
95
+ disable_notification=None,
96
+ reply_to_message_id=None,
97
+ reply_markup=None):
98
+ """ See: https://core.telegram.org/bots/api#sendmessage """
99
+ p = _strip(locals())
100
+ return await self._api_request('sendMessage', _rectify(p))
101
+
102
+ async def forwardMessage(self, chat_id, from_chat_id, message_id,
103
+ disable_notification=None):
104
+ """ See: https://core.telegram.org/bots/api#forwardmessage """
105
+ p = _strip(locals())
106
+ return await self._api_request('forwardMessage', _rectify(p))
107
+
108
+ async def sendPhoto(self, chat_id, photo,
109
+ caption=None,
110
+ parse_mode=None,
111
+ disable_notification=None,
112
+ reply_to_message_id=None,
113
+ reply_markup=None):
114
+ """
115
+ See: https://core.telegram.org/bots/api#sendphoto
116
+
117
+ :param photo:
118
+ - string: ``file_id`` for a photo existing on Telegram servers
119
+ - string: HTTP URL of a photo from the Internet
120
+ - file-like object: obtained by ``open(path, 'rb')``
121
+ - tuple: (filename, file-like object). If the filename contains
122
+ non-ASCII characters and you are using Python 2.7, make sure the
123
+ filename is a unicode string.
124
+ """
125
+ p = _strip(locals(), more=['photo'])
126
+ return await self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo)
127
+
128
+ async def sendAudio(self, chat_id, audio,
129
+ caption=None,
130
+ parse_mode=None,
131
+ duration=None,
132
+ performer=None,
133
+ title=None,
134
+ disable_notification=None,
135
+ reply_to_message_id=None,
136
+ reply_markup=None):
137
+ """
138
+ See: https://core.telegram.org/bots/api#sendaudio
139
+
140
+ :param audio: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
141
+ """
142
+ p = _strip(locals(), more=['audio'])
143
+ return await self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio)
144
+
145
+ async def sendDocument(self, chat_id, document,
146
+ caption=None,
147
+ parse_mode=None,
148
+ disable_notification=None,
149
+ reply_to_message_id=None,
150
+ reply_markup=None):
151
+ """
152
+ See: https://core.telegram.org/bots/api#senddocument
153
+
154
+ :param document: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
155
+ """
156
+ p = _strip(locals(), more=['document'])
157
+ return await self._api_request_with_file('sendDocument', _rectify(p), 'document', document)
158
+
159
+ async def sendVideo(self, chat_id, video,
160
+ duration=None,
161
+ width=None,
162
+ height=None,
163
+ caption=None,
164
+ parse_mode=None,
165
+ supports_streaming=None,
166
+ disable_notification=None,
167
+ reply_to_message_id=None,
168
+ reply_markup=None):
169
+ """
170
+ See: https://core.telegram.org/bots/api#sendvideo
171
+
172
+ :param video: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
173
+ """
174
+ p = _strip(locals(), more=['video'])
175
+ return await self._api_request_with_file('sendVideo', _rectify(p), 'video', video)
176
+
177
+ async def sendVoice(self, chat_id, voice,
178
+ caption=None,
179
+ parse_mode=None,
180
+ duration=None,
181
+ disable_notification=None,
182
+ reply_to_message_id=None,
183
+ reply_markup=None):
184
+ """
185
+ See: https://core.telegram.org/bots/api#sendvoice
186
+
187
+ :param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
188
+ """
189
+ p = _strip(locals(), more=['voice'])
190
+ return await self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice)
191
+
192
+ async def sendVideoNote(self, chat_id, video_note,
193
+ duration=None,
194
+ length=None,
195
+ disable_notification=None,
196
+ reply_to_message_id=None,
197
+ reply_markup=None):
198
+ """
199
+ See: https://core.telegram.org/bots/api#sendvideonote
200
+
201
+ :param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
202
+
203
+ :param length:
204
+ Although marked as optional, this method does not seem to work without
205
+ it being specified. Supply any integer you want. It seems to have no effect
206
+ on the video note's display size.
207
+ """
208
+ p = _strip(locals(), more=['video_note'])
209
+ return await self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note)
210
+
211
+ async def sendMediaGroup(self, chat_id, media,
212
+ disable_notification=None,
213
+ reply_to_message_id=None):
214
+ """
215
+ See: https://core.telegram.org/bots/api#sendmediagroup
216
+
217
+ :type media: array of `InputMedia <https://core.telegram.org/bots/api#inputmedia>`_ objects
218
+ :param media:
219
+ To indicate media locations, each InputMedia object's ``media`` field
220
+ should be one of these:
221
+
222
+ - string: ``file_id`` for a file existing on Telegram servers
223
+ - string: HTTP URL of a file from the Internet
224
+ - file-like object: obtained by ``open(path, 'rb')``
225
+ - tuple: (form-data name, file-like object)
226
+ - tuple: (form-data name, (filename, file-like object))
227
+
228
+ In case of uploading, you may supply customized multipart/form-data
229
+ names for each uploaded file (as in last 2 options above). Otherwise,
230
+ telepot assigns unique names to each uploaded file. Names assigned by
231
+ telepot will not collide with user-supplied names, if any.
232
+ """
233
+ p = _strip(locals(), more=['media'])
234
+ legal_media, files_to_attach = _split_input_media_array(media)
235
+
236
+ p['media'] = legal_media
237
+ return await self._api_request('sendMediaGroup', _rectify(p), files_to_attach)
238
+
239
+ async def sendLocation(self, chat_id, latitude, longitude,
240
+ live_period=None,
241
+ disable_notification=None,
242
+ reply_to_message_id=None,
243
+ reply_markup=None):
244
+ """ See: https://core.telegram.org/bots/api#sendlocation """
245
+ p = _strip(locals())
246
+ return await self._api_request('sendLocation', _rectify(p))
247
+
248
+ async def editMessageLiveLocation(self, msg_identifier, latitude, longitude,
249
+ reply_markup=None):
250
+ """
251
+ See: https://core.telegram.org/bots/api#editmessagelivelocation
252
+
253
+ :param msg_identifier: Same as in :meth:`.Bot.editMessageText`
254
+ """
255
+ p = _strip(locals(), more=['msg_identifier'])
256
+ p.update(_dismantle_message_identifier(msg_identifier))
257
+ return await self._api_request('editMessageLiveLocation', _rectify(p))
258
+
259
+ async def stopMessageLiveLocation(self, msg_identifier,
260
+ reply_markup=None):
261
+ """
262
+ See: https://core.telegram.org/bots/api#stopmessagelivelocation
263
+
264
+ :param msg_identifier: Same as in :meth:`.Bot.editMessageText`
265
+ """
266
+ p = _strip(locals(), more=['msg_identifier'])
267
+ p.update(_dismantle_message_identifier(msg_identifier))
268
+ return await self._api_request('stopMessageLiveLocation', _rectify(p))
269
+
270
+ async def sendVenue(self, chat_id, latitude, longitude, title, address,
271
+ foursquare_id=None,
272
+ disable_notification=None,
273
+ reply_to_message_id=None,
274
+ reply_markup=None):
275
+ """ See: https://core.telegram.org/bots/api#sendvenue """
276
+ p = _strip(locals())
277
+ return await self._api_request('sendVenue', _rectify(p))
278
+
279
+ async def sendContact(self, chat_id, phone_number, first_name,
280
+ last_name=None,
281
+ disable_notification=None,
282
+ reply_to_message_id=None,
283
+ reply_markup=None):
284
+ """ See: https://core.telegram.org/bots/api#sendcontact """
285
+ p = _strip(locals())
286
+ return await self._api_request('sendContact', _rectify(p))
287
+
288
+ async def sendGame(self, chat_id, game_short_name,
289
+ disable_notification=None,
290
+ reply_to_message_id=None,
291
+ reply_markup=None):
292
+ """ See: https://core.telegram.org/bots/api#sendgame """
293
+ p = _strip(locals())
294
+ return await self._api_request('sendGame', _rectify(p))
295
+
296
+ async def sendInvoice(self, chat_id, title, description, payload,
297
+ provider_token, start_parameter, currency, prices,
298
+ provider_data=None,
299
+ photo_url=None,
300
+ photo_size=None,
301
+ photo_width=None,
302
+ photo_height=None,
303
+ need_name=None,
304
+ need_phone_number=None,
305
+ need_email=None,
306
+ need_shipping_address=None,
307
+ is_flexible=None,
308
+ disable_notification=None,
309
+ reply_to_message_id=None,
310
+ reply_markup=None):
311
+ """ See: https://core.telegram.org/bots/api#sendinvoice """
312
+ p = _strip(locals())
313
+ return await self._api_request('sendInvoice', _rectify(p))
314
+
315
+ async def sendChatAction(self, chat_id, action):
316
+ """ See: https://core.telegram.org/bots/api#sendchataction """
317
+ p = _strip(locals())
318
+ return await self._api_request('sendChatAction', _rectify(p))
319
+
320
+ async def getUserProfilePhotos(self, user_id,
321
+ offset=None,
322
+ limit=None):
323
+ """ See: https://core.telegram.org/bots/api#getuserprofilephotos """
324
+ p = _strip(locals())
325
+ return await self._api_request('getUserProfilePhotos', _rectify(p))
326
+
327
+ async def getFile(self, file_id):
328
+ """ See: https://core.telegram.org/bots/api#getfile """
329
+ p = _strip(locals())
330
+ return await self._api_request('getFile', _rectify(p))
331
+
332
+ async def kickChatMember(self, chat_id, user_id,
333
+ until_date=None):
334
+ """ See: https://core.telegram.org/bots/api#kickchatmember """
335
+ p = _strip(locals())
336
+ return await self._api_request('kickChatMember', _rectify(p))
337
+
338
+ async def unbanChatMember(self, chat_id, user_id):
339
+ """ See: https://core.telegram.org/bots/api#unbanchatmember """
340
+ p = _strip(locals())
341
+ return await self._api_request('unbanChatMember', _rectify(p))
342
+
343
+ async def restrictChatMember(self, chat_id, user_id,
344
+ until_date=None,
345
+ can_send_messages=None,
346
+ can_send_media_messages=None,
347
+ can_send_other_messages=None,
348
+ can_add_web_page_previews=None):
349
+ """ See: https://core.telegram.org/bots/api#restrictchatmember """
350
+ p = _strip(locals())
351
+ return await self._api_request('restrictChatMember', _rectify(p))
352
+
353
+ async def promoteChatMember(self, chat_id, user_id,
354
+ can_change_info=None,
355
+ can_post_messages=None,
356
+ can_edit_messages=None,
357
+ can_delete_messages=None,
358
+ can_invite_users=None,
359
+ can_restrict_members=None,
360
+ can_pin_messages=None,
361
+ can_promote_members=None):
362
+ """ See: https://core.telegram.org/bots/api#promotechatmember """
363
+ p = _strip(locals())
364
+ return await self._api_request('promoteChatMember', _rectify(p))
365
+
366
+ async def exportChatInviteLink(self, chat_id):
367
+ """ See: https://core.telegram.org/bots/api#exportchatinvitelink """
368
+ p = _strip(locals())
369
+ return await self._api_request('exportChatInviteLink', _rectify(p))
370
+
371
+ async def setChatPhoto(self, chat_id, photo):
372
+ """ See: https://core.telegram.org/bots/api#setchatphoto """
373
+ p = _strip(locals(), more=['photo'])
374
+ return await self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo)
375
+
376
+ async def deleteChatPhoto(self, chat_id):
377
+ """ See: https://core.telegram.org/bots/api#deletechatphoto """
378
+ p = _strip(locals())
379
+ return await self._api_request('deleteChatPhoto', _rectify(p))
380
+
381
+ async def setChatTitle(self, chat_id, title):
382
+ """ See: https://core.telegram.org/bots/api#setchattitle """
383
+ p = _strip(locals())
384
+ return await self._api_request('setChatTitle', _rectify(p))
385
+
386
+ async def setChatDescription(self, chat_id,
387
+ description=None):
388
+ """ See: https://core.telegram.org/bots/api#setchatdescription """
389
+ p = _strip(locals())
390
+ return await self._api_request('setChatDescription', _rectify(p))
391
+
392
+ async def pinChatMessage(self, chat_id, message_id,
393
+ disable_notification=None):
394
+ """ See: https://core.telegram.org/bots/api#pinchatmessage """
395
+ p = _strip(locals())
396
+ return await self._api_request('pinChatMessage', _rectify(p))
397
+
398
+ async def unpinChatMessage(self, chat_id):
399
+ """ See: https://core.telegram.org/bots/api#unpinchatmessage """
400
+ p = _strip(locals())
401
+ return await self._api_request('unpinChatMessage', _rectify(p))
402
+
403
+ async def leaveChat(self, chat_id):
404
+ """ See: https://core.telegram.org/bots/api#leavechat """
405
+ p = _strip(locals())
406
+ return await self._api_request('leaveChat', _rectify(p))
407
+
408
+ async def getChat(self, chat_id):
409
+ """ See: https://core.telegram.org/bots/api#getchat """
410
+ p = _strip(locals())
411
+ return await self._api_request('getChat', _rectify(p))
412
+
413
+ async def getChatAdministrators(self, chat_id):
414
+ """ See: https://core.telegram.org/bots/api#getchatadministrators """
415
+ p = _strip(locals())
416
+ return await self._api_request('getChatAdministrators', _rectify(p))
417
+
418
+ async def getChatMembersCount(self, chat_id):
419
+ """ See: https://core.telegram.org/bots/api#getchatmemberscount """
420
+ p = _strip(locals())
421
+ return await self._api_request('getChatMembersCount', _rectify(p))
422
+
423
+ async def getChatMember(self, chat_id, user_id):
424
+ """ See: https://core.telegram.org/bots/api#getchatmember """
425
+ p = _strip(locals())
426
+ return await self._api_request('getChatMember', _rectify(p))
427
+
428
+ async def setChatStickerSet(self, chat_id, sticker_set_name):
429
+ """ See: https://core.telegram.org/bots/api#setchatstickerset """
430
+ p = _strip(locals())
431
+ return await self._api_request('setChatStickerSet', _rectify(p))
432
+
433
+ async def deleteChatStickerSet(self, chat_id):
434
+ """ See: https://core.telegram.org/bots/api#deletechatstickerset """
435
+ p = _strip(locals())
436
+ return await self._api_request('deleteChatStickerSet', _rectify(p))
437
+
438
+ async def answerCallbackQuery(self, callback_query_id,
439
+ text=None,
440
+ show_alert=None,
441
+ url=None,
442
+ cache_time=None):
443
+ """ See: https://core.telegram.org/bots/api#answercallbackquery """
444
+ p = _strip(locals())
445
+ return await self._api_request('answerCallbackQuery', _rectify(p))
446
+
447
+ async def answerShippingQuery(self, shipping_query_id, ok,
448
+ shipping_options=None,
449
+ error_message=None):
450
+ """ See: https://core.telegram.org/bots/api#answershippingquery """
451
+ p = _strip(locals())
452
+ return await self._api_request('answerShippingQuery', _rectify(p))
453
+
454
+ async def answerPreCheckoutQuery(self, pre_checkout_query_id, ok,
455
+ error_message=None):
456
+ """ See: https://core.telegram.org/bots/api#answerprecheckoutquery """
457
+ p = _strip(locals())
458
+ return await self._api_request('answerPreCheckoutQuery', _rectify(p))
459
+
460
+ async def editMessageText(self, msg_identifier, text,
461
+ parse_mode=None,
462
+ disable_web_page_preview=None,
463
+ reply_markup=None):
464
+ """
465
+ See: https://core.telegram.org/bots/api#editmessagetext
466
+
467
+ :param msg_identifier:
468
+ a 2-tuple (``chat_id``, ``message_id``),
469
+ a 1-tuple (``inline_message_id``),
470
+ or simply ``inline_message_id``.
471
+ You may extract this value easily with :meth:`telepot.message_identifier`
472
+ """
473
+ p = _strip(locals(), more=['msg_identifier'])
474
+ p.update(_dismantle_message_identifier(msg_identifier))
475
+ return await self._api_request('editMessageText', _rectify(p))
476
+
477
+ async def editMessageCaption(self, msg_identifier,
478
+ caption=None,
479
+ parse_mode=None,
480
+ reply_markup=None):
481
+ """
482
+ See: https://core.telegram.org/bots/api#editmessagecaption
483
+
484
+ :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`
485
+ """
486
+ p = _strip(locals(), more=['msg_identifier'])
487
+ p.update(_dismantle_message_identifier(msg_identifier))
488
+ return await self._api_request('editMessageCaption', _rectify(p))
489
+
490
+ async def editMessageReplyMarkup(self, msg_identifier,
491
+ reply_markup=None):
492
+ """
493
+ See: https://core.telegram.org/bots/api#editmessagereplymarkup
494
+
495
+ :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`
496
+ """
497
+ p = _strip(locals(), more=['msg_identifier'])
498
+ p.update(_dismantle_message_identifier(msg_identifier))
499
+ return await self._api_request('editMessageReplyMarkup', _rectify(p))
500
+
501
+ async def deleteMessage(self, msg_identifier):
502
+ """
503
+ See: https://core.telegram.org/bots/api#deletemessage
504
+
505
+ :param msg_identifier:
506
+ Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`,
507
+ except this method does not work on inline messages.
508
+ """
509
+ p = _strip(locals(), more=['msg_identifier'])
510
+ p.update(_dismantle_message_identifier(msg_identifier))
511
+ return await self._api_request('deleteMessage', _rectify(p))
512
+
513
+ async def sendSticker(self, chat_id, sticker,
514
+ disable_notification=None,
515
+ reply_to_message_id=None,
516
+ reply_markup=None):
517
+ """
518
+ See: https://core.telegram.org/bots/api#sendsticker
519
+
520
+ :param sticker: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto`
521
+ """
522
+ p = _strip(locals(), more=['sticker'])
523
+ return await self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker)
524
+
525
+ async def getStickerSet(self, name):
526
+ """
527
+ See: https://core.telegram.org/bots/api#getstickerset
528
+ """
529
+ p = _strip(locals())
530
+ return await self._api_request('getStickerSet', _rectify(p))
531
+
532
+ async def uploadStickerFile(self, user_id, png_sticker):
533
+ """
534
+ See: https://core.telegram.org/bots/api#uploadstickerfile
535
+ """
536
+ p = _strip(locals(), more=['png_sticker'])
537
+ return await self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker)
538
+
539
+ async def createNewStickerSet(self, user_id, name, title, png_sticker, emojis,
540
+ contains_masks=None,
541
+ mask_position=None):
542
+ """
543
+ See: https://core.telegram.org/bots/api#createnewstickerset
544
+ """
545
+ p = _strip(locals(), more=['png_sticker'])
546
+ return await self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker)
547
+
548
+ async def addStickerToSet(self, user_id, name, png_sticker, emojis,
549
+ mask_position=None):
550
+ """
551
+ See: https://core.telegram.org/bots/api#addstickertoset
552
+ """
553
+ p = _strip(locals(), more=['png_sticker'])
554
+ return await self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker)
555
+
556
+ async def setStickerPositionInSet(self, sticker, position):
557
+ """
558
+ See: https://core.telegram.org/bots/api#setstickerpositioninset
559
+ """
560
+ p = _strip(locals())
561
+ return await self._api_request('setStickerPositionInSet', _rectify(p))
562
+
563
+ async def deleteStickerFromSet(self, sticker):
564
+ """
565
+ See: https://core.telegram.org/bots/api#deletestickerfromset
566
+ """
567
+ p = _strip(locals())
568
+ return await self._api_request('deleteStickerFromSet', _rectify(p))
569
+
570
+ async def answerInlineQuery(self, inline_query_id, results,
571
+ cache_time=None,
572
+ is_personal=None,
573
+ next_offset=None,
574
+ switch_pm_text=None,
575
+ switch_pm_parameter=None):
576
+ """ See: https://core.telegram.org/bots/api#answerinlinequery """
577
+ p = _strip(locals())
578
+ return await self._api_request('answerInlineQuery', _rectify(p))
579
+
580
+ async def getUpdates(self,
581
+ offset=None,
582
+ limit=None,
583
+ timeout=None,
584
+ allowed_updates=None):
585
+ """ See: https://core.telegram.org/bots/api#getupdates """
586
+ p = _strip(locals())
587
+ return await self._api_request('getUpdates', _rectify(p))
588
+
589
+ async def setWebhook(self,
590
+ url=None,
591
+ certificate=None,
592
+ max_connections=None,
593
+ allowed_updates=None):
594
+ """ See: https://core.telegram.org/bots/api#setwebhook """
595
+ p = _strip(locals(), more=['certificate'])
596
+
597
+ if certificate:
598
+ files = {'certificate': certificate}
599
+ return await self._api_request('setWebhook', _rectify(p), files)
600
+ else:
601
+ return await self._api_request('setWebhook', _rectify(p))
602
+
603
+ async def deleteWebhook(self):
604
+ """ See: https://core.telegram.org/bots/api#deletewebhook """
605
+ return await self._api_request('deleteWebhook')
606
+
607
+ async def getWebhookInfo(self):
608
+ """ See: https://core.telegram.org/bots/api#getwebhookinfo """
609
+ return await self._api_request('getWebhookInfo')
610
+
611
+ async def setGameScore(self, user_id, score, game_message_identifier,
612
+ force=None,
613
+ disable_edit_message=None):
614
+ """ See: https://core.telegram.org/bots/api#setgamescore """
615
+ p = _strip(locals(), more=['game_message_identifier'])
616
+ p.update(_dismantle_message_identifier(game_message_identifier))
617
+ return await self._api_request('setGameScore', _rectify(p))
618
+
619
+ async def getGameHighScores(self, user_id, game_message_identifier):
620
+ """ See: https://core.telegram.org/bots/api#getgamehighscores """
621
+ p = _strip(locals(), more=['game_message_identifier'])
622
+ p.update(_dismantle_message_identifier(game_message_identifier))
623
+ return await self._api_request('getGameHighScores', _rectify(p))
624
+
625
+ async def download_file(self, file_id, dest):
626
+ """
627
+ Download a file to local disk.
628
+
629
+ :param dest: a path or a ``file`` object
630
+ """
631
+ f = await self.getFile(file_id)
632
+
633
+ try:
634
+ d = dest if isinstance(dest, io.IOBase) else open(dest, 'wb')
635
+
636
+ session, request = api.download((self._token, f['file_path']))
637
+
638
+ async with session:
639
+ async with request as r:
640
+ while 1:
641
+ chunk = await r.content.read(self._file_chunk_size)
642
+ if not chunk:
643
+ break
644
+ d.write(chunk)
645
+ d.flush()
646
+ finally:
647
+ if not isinstance(dest, io.IOBase) and 'd' in locals():
648
+ d.close()
649
+
650
+ async def message_loop(self, handler=None, relax=0.1,
651
+ timeout=20, allowed_updates=None,
652
+ source=None, ordered=True, maxhold=3):
653
+ """
654
+ Return a task to constantly ``getUpdates`` or pull updates from a queue.
655
+ Apply ``handler`` to every message received.
656
+
657
+ :param handler:
658
+ a function that takes one argument (the message), or a routing table.
659
+ If ``None``, the bot's ``handle`` method is used.
660
+
661
+ A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate
662
+ handler functions according to their flavors. It allows you to define functions specifically
663
+ to handle one flavor of messages. It usually looks like this: ``{'chat': fn1,
664
+ 'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take
665
+ one argument (the message).
666
+
667
+ :param source:
668
+ Source of updates.
669
+ If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers.
670
+ If it is a ``asyncio.Queue``, new messages are pulled from the queue.
671
+ A web application implementing a webhook can dump updates into the queue,
672
+ while the bot pulls from it. This is how telepot can be integrated with webhooks.
673
+
674
+ Acceptable contents in queue:
675
+
676
+ - ``str`` or ``bytes`` (decoded using UTF-8)
677
+ representing a JSON-serialized `Update <https://core.telegram.org/bots/api#update>`_ object.
678
+ - a ``dict`` representing an Update object.
679
+
680
+ When ``source`` is a queue, these parameters are meaningful:
681
+
682
+ :type ordered: bool
683
+ :param ordered:
684
+ If ``True``, ensure in-order delivery of messages to ``handler``
685
+ (i.e. updates with a smaller ``update_id`` always come before those with
686
+ a larger ``update_id``).
687
+ If ``False``, no re-ordering is done. ``handler`` is applied to messages
688
+ as soon as they are pulled from queue.
689
+
690
+ :type maxhold: float
691
+ :param maxhold:
692
+ Applied only when ``ordered`` is ``True``. The maximum number of seconds
693
+ an update is held waiting for a not-yet-arrived smaller ``update_id``.
694
+ When this number of seconds is up, the update is delivered to ``handler``
695
+ even if some smaller ``update_id``\s have not yet arrived. If those smaller
696
+ ``update_id``\s arrive at some later time, they are discarded.
697
+
698
+ :type timeout: int
699
+ :param timeout:
700
+ ``timeout`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`,
701
+ controlling how long to poll in seconds.
702
+
703
+ :type allowed_updates: array of string
704
+ :param allowed_updates:
705
+ ``allowed_updates`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`,
706
+ controlling which types of updates to receive.
707
+ """
708
+ if handler is None:
709
+ handler = self.handle
710
+ elif isinstance(handler, dict):
711
+ handler = flavor_router(handler)
712
+
713
+ def create_task_for(msg):
714
+ self.loop.create_task(handler(msg))
715
+
716
+ if asyncio.iscoroutinefunction(handler):
717
+ callback = create_task_for
718
+ else:
719
+ callback = handler
720
+
721
+ def handle(update):
722
+ try:
723
+ key = _find_first_key(update, ['message',
724
+ 'edited_message',
725
+ 'channel_post',
726
+ 'edited_channel_post',
727
+ 'callback_query',
728
+ 'inline_query',
729
+ 'chosen_inline_result',
730
+ 'shipping_query',
731
+ 'pre_checkout_query'])
732
+
733
+ callback(update[key])
734
+ except:
735
+ # Localize the error so message thread can keep going.
736
+ traceback.print_exc()
737
+ finally:
738
+ return update['update_id']
739
+
740
+ async def get_from_telegram_server():
741
+ offset = None # running offset
742
+ allowed_upd = allowed_updates
743
+ while 1:
744
+ try:
745
+ result = await self.getUpdates(offset=offset,
746
+ timeout=timeout,
747
+ allowed_updates=allowed_upd)
748
+
749
+ # Once passed, this parameter is no longer needed.
750
+ allowed_upd = None
751
+
752
+ if len(result) > 0:
753
+ # No sort. Trust server to give messages in correct order.
754
+ # Update offset to max(update_id) + 1
755
+ offset = max([handle(update) for update in result]) + 1
756
+ except CancelledError:
757
+ raise
758
+ except exception.BadHTTPResponse as e:
759
+ traceback.print_exc()
760
+
761
+ # Servers probably down. Wait longer.
762
+ if e.status == 502:
763
+ await asyncio.sleep(30)
764
+ except:
765
+ traceback.print_exc()
766
+ await asyncio.sleep(relax)
767
+ else:
768
+ await asyncio.sleep(relax)
769
+
770
+ def dictify(data):
771
+ if type(data) is bytes:
772
+ return json.loads(data.decode('utf-8'))
773
+ elif type(data) is str:
774
+ return json.loads(data)
775
+ elif type(data) is dict:
776
+ return data
777
+ else:
778
+ raise ValueError()
779
+
780
+ async def get_from_queue_unordered(qu):
781
+ while 1:
782
+ try:
783
+ data = await qu.get()
784
+ update = dictify(data)
785
+ handle(update)
786
+ except:
787
+ traceback.print_exc()
788
+
789
+ async def get_from_queue(qu):
790
+ # Here is the re-ordering mechanism, ensuring in-order delivery of updates.
791
+ max_id = None # max update_id passed to callback
792
+ buffer = collections.deque() # keep those updates which skip some update_id
793
+ qwait = None # how long to wait for updates,
794
+ # because buffer's content has to be returned in time.
795
+
796
+ while 1:
797
+ try:
798
+ data = await asyncio.wait_for(qu.get(), qwait)
799
+ update = dictify(data)
800
+
801
+ if max_id is None:
802
+ # First message received, handle regardless.
803
+ max_id = handle(update)
804
+
805
+ elif update['update_id'] == max_id + 1:
806
+ # No update_id skipped, handle naturally.
807
+ max_id = handle(update)
808
+
809
+ # clear contagious updates in buffer
810
+ if len(buffer) > 0:
811
+ buffer.popleft() # first element belongs to update just received, useless now.
812
+ while 1:
813
+ try:
814
+ if type(buffer[0]) is dict:
815
+ max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them.
816
+ else:
817
+ break # gap, no more contagious updates
818
+ except IndexError:
819
+ break # buffer empty
820
+
821
+ elif update['update_id'] > max_id + 1:
822
+ # Update arrives pre-maturely, insert to buffer.
823
+ nbuf = len(buffer)
824
+ if update['update_id'] <= max_id + nbuf:
825
+ # buffer long enough, put update at position
826
+ buffer[update['update_id'] - max_id - 1] = update
827
+ else:
828
+ # buffer too short, lengthen it
829
+ expire = time.time() + maxhold
830
+ for a in range(nbuf, update['update_id']-max_id-1):
831
+ buffer.append(expire) # put expiry time in gaps
832
+ buffer.append(update)
833
+
834
+ else:
835
+ pass # discard
836
+
837
+ except asyncio.TimeoutError:
838
+ # debug message
839
+ # print('Timeout')
840
+
841
+ # some buffer contents have to be handled
842
+ # flush buffer until a non-expired time is encountered
843
+ while 1:
844
+ try:
845
+ if type(buffer[0]) is dict:
846
+ max_id = handle(buffer.popleft())
847
+ else:
848
+ expire = buffer[0]
849
+ if expire <= time.time():
850
+ max_id += 1
851
+ buffer.popleft()
852
+ else:
853
+ break # non-expired
854
+ except IndexError:
855
+ break # buffer empty
856
+ except:
857
+ traceback.print_exc()
858
+ finally:
859
+ try:
860
+ # don't wait longer than next expiry time
861
+ qwait = buffer[0] - time.time()
862
+ if qwait < 0:
863
+ qwait = 0
864
+ except IndexError:
865
+ # buffer empty, can wait forever
866
+ qwait = None
867
+
868
+ # debug message
869
+ # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
870
+
871
+ self._scheduler._callback = callback
872
+
873
+ if source is None:
874
+ await get_from_telegram_server()
875
+ elif isinstance(source, asyncio.Queue):
876
+ if ordered:
877
+ await get_from_queue(source)
878
+ else:
879
+ await get_from_queue_unordered(source)
880
+ else:
881
+ raise ValueError('Invalid source')
882
+
883
+
884
+ class SpeakerBot(Bot):
885
+ def __init__(self, token, loop=None):
886
+ super(SpeakerBot, self).__init__(token, loop)
887
+ self._mic = helper.Microphone()
888
+
889
+ @property
890
+ def mic(self):
891
+ return self._mic
892
+
893
+ def create_listener(self):
894
+ q = asyncio.Queue()
895
+ self._mic.add(q)
896
+ ln = helper.Listener(self._mic, q)
897
+ return ln
898
+
899
+
900
+ class DelegatorBot(SpeakerBot):
901
+ def __init__(self, token, delegation_patterns, loop=None):
902
+ """
903
+ :param delegation_patterns: a list of (seeder, delegator) tuples.
904
+ """
905
+ super(DelegatorBot, self).__init__(token, loop)
906
+ self._delegate_records = [p+({},) for p in delegation_patterns]
907
+
908
+ def handle(self, msg):
909
+ self._mic.send(msg)
910
+
911
+ for calculate_seed, make_coroutine_obj, dict in self._delegate_records:
912
+ id = calculate_seed(msg)
913
+
914
+ if id is None:
915
+ continue
916
+ elif isinstance(id, collections.Hashable):
917
+ if id not in dict or dict[id].done():
918
+ c = make_coroutine_obj((self, msg, id))
919
+
920
+ if not asyncio.iscoroutine(c):
921
+ raise RuntimeError('You must produce a coroutine *object* as delegate.')
922
+
923
+ dict[id] = self._loop.create_task(c)
924
+ else:
925
+ c = make_coroutine_obj((self, msg, id))
926
+ self._loop.create_task(c)
telepot/aio/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (52.8 kB). View file
 
telepot/aio/__pycache__/api.cpython-311.pyc ADDED
Binary file (9.26 kB). View file
 
telepot/aio/__pycache__/delegate.cpython-311.pyc ADDED
Binary file (5.83 kB). View file
 
telepot/aio/__pycache__/hack.cpython-311.pyc ADDED
Binary file (1.96 kB). View file
 
telepot/aio/__pycache__/helper.cpython-311.pyc ADDED
Binary file (23.2 kB). View file
 
telepot/aio/__pycache__/loop.cpython-311.pyc ADDED
Binary file (10 kB). View file
 
telepot/aio/__pycache__/routing.cpython-311.pyc ADDED
Binary file (2.66 kB). View file
 
telepot/aio/api.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import aiohttp
3
+ import async_timeout
4
+ import atexit
5
+ import re
6
+ import json
7
+ from .. import exception
8
+ from ..api import _methodurl, _which_pool, _fileurl, _guess_filename
9
+
10
+ _loop = asyncio.get_event_loop()
11
+
12
+ _pools = {
13
+ 'default': aiohttp.ClientSession(
14
+ connector=aiohttp.TCPConnector(limit=10),
15
+ loop=_loop)
16
+ }
17
+
18
+ _timeout = 30
19
+ _proxy = None # (url, (username, password))
20
+
21
+ def set_proxy(url, basic_auth=None):
22
+ global _proxy
23
+ if not url:
24
+ _proxy = None
25
+ else:
26
+ _proxy = (url, basic_auth) if basic_auth else (url,)
27
+
28
+ def _proxy_kwargs():
29
+ if _proxy is None or len(_proxy) == 0:
30
+ return {}
31
+ elif len(_proxy) == 1:
32
+ return {'proxy': _proxy[0]}
33
+ elif len(_proxy) == 2:
34
+ return {'proxy': _proxy[0], 'proxy_auth': aiohttp.BasicAuth(*_proxy[1])}
35
+ else:
36
+ raise RuntimeError("_proxy has invalid length")
37
+
38
+ async def _close_pools():
39
+ global _pools
40
+ for s in _pools.values():
41
+ await s.close()
42
+
43
+ atexit.register(lambda: _loop.create_task(_close_pools())) # have to wrap async function
44
+
45
+ def _create_onetime_pool():
46
+ return aiohttp.ClientSession(
47
+ connector=aiohttp.TCPConnector(limit=1, force_close=True),
48
+ loop=_loop)
49
+
50
+ def _default_timeout(req, **user_kw):
51
+ return _timeout
52
+
53
+ def _compose_timeout(req, **user_kw):
54
+ token, method, params, files = req
55
+
56
+ if method == 'getUpdates' and params and 'timeout' in params:
57
+ # Ensure HTTP timeout is longer than getUpdates timeout
58
+ return params['timeout'] + _default_timeout(req, **user_kw)
59
+ elif files:
60
+ # Disable timeout if uploading files. For some reason, the larger the file,
61
+ # the longer it takes for the server to respond (after upload is finished).
62
+ # It is unclear how long timeout should be.
63
+ return None
64
+ else:
65
+ return _default_timeout(req, **user_kw)
66
+
67
+ def _compose_data(req, **user_kw):
68
+ token, method, params, files = req
69
+
70
+ data = aiohttp.FormData()
71
+
72
+ if params:
73
+ for key,value in params.items():
74
+ data.add_field(key, str(value))
75
+
76
+ if files:
77
+ for key,f in files.items():
78
+ if isinstance(f, tuple):
79
+ if len(f) == 2:
80
+ filename, fileobj = f
81
+ else:
82
+ raise ValueError('Tuple must have exactly 2 elements: filename, fileobj')
83
+ else:
84
+ filename, fileobj = _guess_filename(f) or key, f
85
+
86
+ data.add_field(key, fileobj, filename=filename)
87
+
88
+ return data
89
+
90
+ def _transform(req, **user_kw):
91
+ timeout = _compose_timeout(req, **user_kw)
92
+
93
+ data = _compose_data(req, **user_kw)
94
+
95
+ url = _methodurl(req, **user_kw)
96
+
97
+ name = _which_pool(req, **user_kw)
98
+
99
+ if name is None:
100
+ session = _create_onetime_pool()
101
+ cleanup = session.close # one-time session: remember to close
102
+ else:
103
+ session = _pools[name]
104
+ cleanup = None # reuse: do not close
105
+
106
+ kwargs = {'data':data}
107
+ kwargs.update(user_kw)
108
+
109
+ return session.post, (url,), kwargs, timeout, cleanup
110
+
111
+ async def _parse(response):
112
+ try:
113
+ data = await response.json()
114
+ if data is None:
115
+ raise ValueError()
116
+ except (ValueError, json.JSONDecodeError, aiohttp.ClientResponseError):
117
+ text = await response.text()
118
+ raise exception.BadHTTPResponse(response.status, text, response)
119
+
120
+ if data['ok']:
121
+ return data['result']
122
+ else:
123
+ description, error_code = data['description'], data['error_code']
124
+
125
+ # Look for specific error ...
126
+ for e in exception.TelegramError.__subclasses__():
127
+ n = len(e.DESCRIPTION_PATTERNS)
128
+ if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])):
129
+ raise e(description, error_code, data)
130
+
131
+ # ... or raise generic error
132
+ raise exception.TelegramError(description, error_code, data)
133
+
134
+ async def request(req, **user_kw):
135
+ fn, args, kwargs, timeout, cleanup = _transform(req, **user_kw)
136
+
137
+ kwargs.update(_proxy_kwargs())
138
+ try:
139
+ if timeout is None:
140
+ async with fn(*args, **kwargs) as r:
141
+ return await _parse(r)
142
+ else:
143
+ try:
144
+ with async_timeout.timeout(timeout):
145
+ async with fn(*args, **kwargs) as r:
146
+ return await _parse(r)
147
+
148
+ except asyncio.TimeoutError:
149
+ raise exception.TelegramError('Response timeout', 504, {})
150
+
151
+ except aiohttp.ClientConnectionError:
152
+ raise exception.TelegramError('Connection Error', 400, {})
153
+
154
+ finally:
155
+ if cleanup: # e.g. closing one-time session
156
+ if asyncio.iscoroutinefunction(cleanup):
157
+ await cleanup()
158
+ else:
159
+ cleanup()
160
+
161
+ def download(req):
162
+ session = _create_onetime_pool()
163
+
164
+ kwargs = {}
165
+ kwargs.update(_proxy_kwargs())
166
+
167
+ return session, session.get(_fileurl(req), timeout=_timeout, **kwargs)
168
+ # Caller should close session after download is complete
telepot/aio/delegate.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Like :mod:`telepot.delegate`, this module has a bunch of seeder factories
3
+ and delegator factories.
4
+
5
+ .. autofunction:: per_chat_id
6
+ .. autofunction:: per_chat_id_in
7
+ .. autofunction:: per_chat_id_except
8
+ .. autofunction:: per_from_id
9
+ .. autofunction:: per_from_id_in
10
+ .. autofunction:: per_from_id_except
11
+ .. autofunction:: per_inline_from_id
12
+ .. autofunction:: per_inline_from_id_in
13
+ .. autofunction:: per_inline_from_id_except
14
+ .. autofunction:: per_application
15
+ .. autofunction:: per_message
16
+ .. autofunction:: per_event_source_id
17
+ .. autofunction:: per_callback_query_chat_id
18
+ .. autofunction:: per_callback_query_origin
19
+ .. autofunction:: per_invoice_payload
20
+ .. autofunction:: until
21
+ .. autofunction:: chain
22
+ .. autofunction:: pair
23
+ .. autofunction:: pave_event_space
24
+ .. autofunction:: include_callback_query_chat_id
25
+ .. autofunction:: intercept_callback_query_origin
26
+ """
27
+
28
+ import asyncio
29
+ import traceback
30
+ from .. import exception
31
+ from . import helper
32
+
33
+ # Mirror traditional version to avoid having to import one more module
34
+ from ..delegate import (
35
+ per_chat_id, per_chat_id_in, per_chat_id_except,
36
+ per_from_id, per_from_id_in, per_from_id_except,
37
+ per_inline_from_id, per_inline_from_id_in, per_inline_from_id_except,
38
+ per_application, per_message, per_event_source_id,
39
+ per_callback_query_chat_id, per_callback_query_origin, per_invoice_payload,
40
+ until, chain, pair, pave_event_space,
41
+ include_callback_query_chat_id, intercept_callback_query_origin
42
+ )
43
+
44
+ def _ensure_coroutine_function(fn):
45
+ return fn if asyncio.iscoroutinefunction(fn) else asyncio.coroutine(fn)
46
+
47
+ def call(corofunc, *args, **kwargs):
48
+ """
49
+ :return:
50
+ a delegator function that returns a coroutine object by calling
51
+ ``corofunc(seed_tuple, *args, **kwargs)``.
52
+ """
53
+ corofunc = _ensure_coroutine_function(corofunc)
54
+ def f(seed_tuple):
55
+ return corofunc(seed_tuple, *args, **kwargs)
56
+ return f
57
+
58
+ def create_run(cls, *args, **kwargs):
59
+ """
60
+ :return:
61
+ a delegator function that calls the ``cls`` constructor whose arguments being
62
+ a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
63
+ a coroutine object by calling the object's ``run`` method, which should be
64
+ a coroutine function.
65
+ """
66
+ def f(seed_tuple):
67
+ j = cls(seed_tuple, *args, **kwargs)
68
+ return _ensure_coroutine_function(j.run)()
69
+ return f
70
+
71
+ def create_open(cls, *args, **kwargs):
72
+ """
73
+ :return:
74
+ a delegator function that calls the ``cls`` constructor whose arguments being
75
+ a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
76
+ a looping coroutine object that uses the object's ``listener`` to wait for
77
+ messages and invokes instance method ``open``, ``on_message``, and ``on_close``
78
+ accordingly.
79
+ """
80
+ def f(seed_tuple):
81
+ j = cls(seed_tuple, *args, **kwargs)
82
+
83
+ async def wait_loop():
84
+ bot, msg, seed = seed_tuple
85
+ try:
86
+ handled = await helper._invoke(j.open, msg, seed)
87
+ if not handled:
88
+ await helper._invoke(j.on_message, msg)
89
+
90
+ while 1:
91
+ msg = await j.listener.wait()
92
+ await helper._invoke(j.on_message, msg)
93
+
94
+ # These exceptions are "normal" exits.
95
+ except (exception.IdleTerminate, exception.StopListening) as e:
96
+ await helper._invoke(j.on_close, e)
97
+
98
+ # Any other exceptions are accidents. **Print it out.**
99
+ # This is to prevent swallowing exceptions in the case that on_close()
100
+ # gets overridden but fails to account for unexpected exceptions.
101
+ except Exception as e:
102
+ traceback.print_exc()
103
+ await helper._invoke(j.on_close, e)
104
+
105
+ return wait_loop()
106
+ return f
telepot/aio/hack.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ import aiohttp
3
+ from urllib.parse import quote
4
+
5
+ def content_disposition_header(disptype, quote_fields=True, **params):
6
+ if not disptype or not (aiohttp.helpers.TOKEN > set(disptype)):
7
+ raise ValueError('bad content disposition type {!r}'
8
+ ''.format(disptype))
9
+
10
+ value = disptype
11
+ if params:
12
+ lparams = []
13
+ for key, val in params.items():
14
+ if not key or not (aiohttp.helpers.TOKEN > set(key)):
15
+ raise ValueError('bad content disposition parameter'
16
+ ' {!r}={!r}'.format(key, val))
17
+
18
+ ###### Do not encode filename
19
+ if key == 'filename':
20
+ qval = val
21
+ else:
22
+ qval = quote(val, '') if quote_fields else val
23
+
24
+ lparams.append((key, '"%s"' % qval))
25
+
26
+ sparams = '; '.join('='.join(pair) for pair in lparams)
27
+ value = '; '.join((value, sparams))
28
+ return value
29
+
30
+ # Override original version
31
+ aiohttp.payload.content_disposition_header = content_disposition_header
32
+
33
+ # In case aiohttp changes and this hack no longer works, I don't want it to
34
+ # bog down the entire library.
35
+ except (ImportError, AttributeError):
36
+ pass
telepot/aio/helper.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import traceback
3
+ from .. import filtering, helper, exception
4
+ from .. import (
5
+ flavor, chat_flavors, inline_flavors, is_event,
6
+ message_identifier, origin_identifier)
7
+
8
+ # Mirror traditional version
9
+ from ..helper import (
10
+ Sender, Administrator, Editor, openable,
11
+ StandardEventScheduler, StandardEventMixin)
12
+
13
+
14
+ async def _invoke(fn, *args, **kwargs):
15
+ if asyncio.iscoroutinefunction(fn):
16
+ return await fn(*args, **kwargs)
17
+ else:
18
+ return fn(*args, **kwargs)
19
+
20
+
21
+ def _create_invoker(obj, method_name):
22
+ async def d(*a, **kw):
23
+ method = getattr(obj, method_name)
24
+ return await _invoke(method, *a, **kw)
25
+ return d
26
+
27
+
28
+ class Microphone(object):
29
+ def __init__(self):
30
+ self._queues = set()
31
+
32
+ def add(self, q):
33
+ self._queues.add(q)
34
+
35
+ def remove(self, q):
36
+ self._queues.remove(q)
37
+
38
+ def send(self, msg):
39
+ for q in self._queues:
40
+ try:
41
+ q.put_nowait(msg)
42
+ except asyncio.QueueFull:
43
+ traceback.print_exc()
44
+ pass
45
+
46
+
47
+ class Listener(helper.Listener):
48
+ async def wait(self):
49
+ """
50
+ Block until a matched message appears.
51
+ """
52
+ if not self._patterns:
53
+ raise RuntimeError('Listener has nothing to capture')
54
+
55
+ while 1:
56
+ msg = await self._queue.get()
57
+
58
+ if any(map(lambda p: filtering.match_all(msg, p), self._patterns)):
59
+ return msg
60
+
61
+
62
+ from concurrent.futures._base import CancelledError
63
+
64
+ class Answerer(object):
65
+ """
66
+ When processing inline queries, ensures **at most one active task** per user id.
67
+ """
68
+
69
+ def __init__(self, bot, loop=None):
70
+ self._bot = bot
71
+ self._loop = loop if loop is not None else asyncio.get_event_loop()
72
+ self._working_tasks = {}
73
+
74
+ def answer(self, inline_query, compute_fn, *compute_args, **compute_kwargs):
75
+ """
76
+ Create a task that calls ``compute fn`` (along with additional arguments
77
+ ``*compute_args`` and ``**compute_kwargs``), then applies the returned value to
78
+ :meth:`.Bot.answerInlineQuery` to answer the inline query.
79
+ If a preceding task is already working for a user, that task is cancelled,
80
+ thus ensuring at most one active task per user id.
81
+
82
+ :param inline_query:
83
+ The inline query to be processed. The originating user is inferred from ``msg['from']['id']``.
84
+
85
+ :param compute_fn:
86
+ A function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send.
87
+ May return:
88
+
89
+ - a *list* of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_
90
+ - a *tuple* whose first element is a list of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_,
91
+ followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery`
92
+ - a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery`
93
+
94
+ :param \*compute_args: positional arguments to ``compute_fn``
95
+ :param \*\*compute_kwargs: keyword arguments to ``compute_fn``
96
+ """
97
+
98
+ from_id = inline_query['from']['id']
99
+
100
+ async def compute_and_answer():
101
+ try:
102
+ query_id = inline_query['id']
103
+
104
+ ans = await _invoke(compute_fn, *compute_args, **compute_kwargs)
105
+
106
+ if isinstance(ans, list):
107
+ await self._bot.answerInlineQuery(query_id, ans)
108
+ elif isinstance(ans, tuple):
109
+ await self._bot.answerInlineQuery(query_id, *ans)
110
+ elif isinstance(ans, dict):
111
+ await self._bot.answerInlineQuery(query_id, **ans)
112
+ else:
113
+ raise ValueError('Invalid answer format')
114
+ except CancelledError:
115
+ # Cancelled. Record has been occupied by new task. Don't touch.
116
+ raise
117
+ except:
118
+ # Die accidentally. Remove myself from record.
119
+ del self._working_tasks[from_id]
120
+ raise
121
+ else:
122
+ # Die naturally. Remove myself from record.
123
+ del self._working_tasks[from_id]
124
+
125
+ if from_id in self._working_tasks:
126
+ self._working_tasks[from_id].cancel()
127
+
128
+ t = self._loop.create_task(compute_and_answer())
129
+ self._working_tasks[from_id] = t
130
+
131
+
132
+ class AnswererMixin(helper.AnswererMixin):
133
+ Answerer = Answerer # use async Answerer class
134
+
135
+
136
+ class CallbackQueryCoordinator(helper.CallbackQueryCoordinator):
137
+ def augment_send(self, send_func):
138
+ async def augmented(*aa, **kw):
139
+ sent = await send_func(*aa, **kw)
140
+
141
+ if self._enable_chat and self._contains_callback_data(kw):
142
+ self.capture_origin(message_identifier(sent))
143
+
144
+ return sent
145
+ return augmented
146
+
147
+ def augment_edit(self, edit_func):
148
+ async def augmented(msg_identifier, *aa, **kw):
149
+ edited = await edit_func(msg_identifier, *aa, **kw)
150
+
151
+ if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat):
152
+ if self._contains_callback_data(kw):
153
+ self.capture_origin(msg_identifier)
154
+ else:
155
+ self.uncapture_origin(msg_identifier)
156
+
157
+ return edited
158
+ return augmented
159
+
160
+ def augment_delete(self, delete_func):
161
+ async def augmented(msg_identifier, *aa, **kw):
162
+ deleted = await delete_func(msg_identifier, *aa, **kw)
163
+
164
+ if deleted is True:
165
+ self.uncapture_origin(msg_identifier)
166
+
167
+ return deleted
168
+ return augmented
169
+
170
+ def augment_on_message(self, handler):
171
+ async def augmented(msg):
172
+ if (self._enable_inline
173
+ and flavor(msg) == 'chosen_inline_result'
174
+ and 'inline_message_id' in msg):
175
+ inline_message_id = msg['inline_message_id']
176
+ self.capture_origin(inline_message_id)
177
+
178
+ return await _invoke(handler, msg)
179
+ return augmented
180
+
181
+
182
+ class InterceptCallbackQueryMixin(helper.InterceptCallbackQueryMixin):
183
+ CallbackQueryCoordinator = CallbackQueryCoordinator
184
+
185
+
186
+ class IdleEventCoordinator(helper.IdleEventCoordinator):
187
+ def augment_on_message(self, handler):
188
+ async def augmented(msg):
189
+ # Reset timer if this is an external message
190
+ is_event(msg) or self.refresh()
191
+ return await _invoke(handler, msg)
192
+ return augmented
193
+
194
+ def augment_on_close(self, handler):
195
+ async def augmented(ex):
196
+ try:
197
+ if self._timeout_event:
198
+ self._scheduler.cancel(self._timeout_event)
199
+ self._timeout_event = None
200
+ # This closing may have been caused by my own timeout, in which case
201
+ # the timeout event can no longer be found in the scheduler.
202
+ except exception.EventNotFound:
203
+ self._timeout_event = None
204
+ return await _invoke(handler, ex)
205
+ return augmented
206
+
207
+
208
+ class IdleTerminateMixin(helper.IdleTerminateMixin):
209
+ IdleEventCoordinator = IdleEventCoordinator
210
+
211
+
212
+ class Router(helper.Router):
213
+ async def route(self, msg, *aa, **kw):
214
+ """
215
+ Apply key function to ``msg`` to obtain a key, look up routing table
216
+ to obtain a handler function, then call the handler function with
217
+ positional and keyword arguments, if any is returned by the key function.
218
+
219
+ ``*aa`` and ``**kw`` are dummy placeholders for easy nesting.
220
+ Regardless of any number of arguments returned by the key function,
221
+ multi-level routing may be achieved like this::
222
+
223
+ top_router.routing_table['key1'] = sub_router1.route
224
+ top_router.routing_table['key2'] = sub_router2.route
225
+ """
226
+ k = self.key_function(msg)
227
+
228
+ if isinstance(k, (tuple, list)):
229
+ key, args, kwargs = {1: tuple(k) + ((),{}),
230
+ 2: tuple(k) + ({},),
231
+ 3: tuple(k),}[len(k)]
232
+ else:
233
+ key, args, kwargs = k, (), {}
234
+
235
+ try:
236
+ fn = self.routing_table[key]
237
+ except KeyError as e:
238
+ # Check for default handler, key=None
239
+ if None in self.routing_table:
240
+ fn = self.routing_table[None]
241
+ else:
242
+ raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args))
243
+
244
+ return await _invoke(fn, msg, *args, **kwargs)
245
+
246
+
247
+ class DefaultRouterMixin(object):
248
+ def __init__(self, *args, **kwargs):
249
+ self._router = Router(flavor, {'chat': _create_invoker(self, 'on_chat_message'),
250
+ 'callback_query': _create_invoker(self, 'on_callback_query'),
251
+ 'inline_query': _create_invoker(self, 'on_inline_query'),
252
+ 'chosen_inline_result': _create_invoker(self, 'on_chosen_inline_result'),
253
+ 'shipping_query': _create_invoker(self, 'on_shipping_query'),
254
+ 'pre_checkout_query': _create_invoker(self, 'on_pre_checkout_query'),
255
+ '_idle': _create_invoker(self, 'on__idle')})
256
+
257
+ super(DefaultRouterMixin, self).__init__(*args, **kwargs)
258
+
259
+ @property
260
+ def router(self):
261
+ """ See :class:`.helper.Router` """
262
+ return self._router
263
+
264
+ async def on_message(self, msg):
265
+ """
266
+ Called when a message is received.
267
+ By default, call :meth:`Router.route` to handle the message.
268
+ """
269
+ await self._router.route(msg)
270
+
271
+
272
+ @openable
273
+ class Monitor(helper.ListenerContext, DefaultRouterMixin):
274
+ def __init__(self, seed_tuple, capture, **kwargs):
275
+ """
276
+ A delegate that never times-out, probably doing some kind of background monitoring
277
+ in the application. Most naturally paired with :func:`telepot.aio.delegate.per_application`.
278
+
279
+ :param capture: a list of patterns for ``listener`` to capture
280
+ """
281
+ bot, initial_msg, seed = seed_tuple
282
+ super(Monitor, self).__init__(bot, seed, **kwargs)
283
+
284
+ for pattern in capture:
285
+ self.listener.capture(pattern)
286
+
287
+
288
+ @openable
289
+ class ChatHandler(helper.ChatContext,
290
+ DefaultRouterMixin,
291
+ StandardEventMixin,
292
+ IdleTerminateMixin):
293
+ def __init__(self, seed_tuple,
294
+ include_callback_query=False, **kwargs):
295
+ """
296
+ A delegate to handle a chat.
297
+ """
298
+ bot, initial_msg, seed = seed_tuple
299
+ super(ChatHandler, self).__init__(bot, seed, **kwargs)
300
+
301
+ self.listener.capture([{'chat': {'id': self.chat_id}}])
302
+
303
+ if include_callback_query:
304
+ self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}])
305
+
306
+
307
+ @openable
308
+ class UserHandler(helper.UserContext,
309
+ DefaultRouterMixin,
310
+ StandardEventMixin,
311
+ IdleTerminateMixin):
312
+ def __init__(self, seed_tuple,
313
+ include_callback_query=False,
314
+ flavors=chat_flavors+inline_flavors, **kwargs):
315
+ """
316
+ A delegate to handle a user's actions.
317
+
318
+ :param flavors:
319
+ A list of flavors to capture. ``all`` covers all flavors.
320
+ """
321
+ bot, initial_msg, seed = seed_tuple
322
+ super(UserHandler, self).__init__(bot, seed, **kwargs)
323
+
324
+ if flavors == 'all':
325
+ self.listener.capture([{'from': {'id': self.user_id}}])
326
+ else:
327
+ self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}])
328
+
329
+ if include_callback_query:
330
+ self.listener.capture([{'message': {'chat': {'id': self.user_id}}}])
331
+
332
+
333
+ class InlineUserHandler(UserHandler):
334
+ def __init__(self, seed_tuple, **kwargs):
335
+ """
336
+ A delegate to handle a user's inline-related actions.
337
+ """
338
+ super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs)
339
+
340
+
341
+ @openable
342
+ class CallbackQueryOriginHandler(helper.CallbackQueryOriginContext,
343
+ DefaultRouterMixin,
344
+ StandardEventMixin,
345
+ IdleTerminateMixin):
346
+ def __init__(self, seed_tuple, **kwargs):
347
+ """
348
+ A delegate to handle callback query from one origin.
349
+ """
350
+ bot, initial_msg, seed = seed_tuple
351
+ super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs)
352
+
353
+ self.listener.capture([
354
+ lambda msg:
355
+ flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin
356
+ ])
357
+
358
+
359
+ @openable
360
+ class InvoiceHandler(helper.InvoiceContext,
361
+ DefaultRouterMixin,
362
+ StandardEventMixin,
363
+ IdleTerminateMixin):
364
+ def __init__(self, seed_tuple, **kwargs):
365
+ """
366
+ A delegate to handle messages related to an invoice.
367
+ """
368
+ bot, initial_msg, seed = seed_tuple
369
+ super(InvoiceHandler, self).__init__(bot, seed, **kwargs)
370
+
371
+ self.listener.capture([{'invoice_payload': self.payload}])
372
+ self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}])
telepot/aio/loop.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import time
3
+ import traceback
4
+ import collections
5
+ from concurrent.futures._base import CancelledError
6
+
7
+ from . import flavor_router
8
+
9
+ from ..loop import _extract_message, _dictify
10
+ from .. import exception
11
+
12
+
13
+ class GetUpdatesLoop(object):
14
+ def __init__(self, bot, on_update):
15
+ self._bot = bot
16
+ self._update_handler = on_update
17
+
18
+ async def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None):
19
+ """
20
+ Process new updates in infinity loop
21
+
22
+ :param relax: float
23
+ :param offset: int
24
+ :param timeout: int
25
+ :param allowed_updates: bool
26
+ """
27
+ while 1:
28
+ try:
29
+ result = await self._bot.getUpdates(offset=offset,
30
+ timeout=timeout,
31
+ allowed_updates=allowed_updates)
32
+
33
+ # Once passed, this parameter is no longer needed.
34
+ allowed_updates = None
35
+
36
+ # No sort. Trust server to give messages in correct order.
37
+ for update in result:
38
+ self._update_handler(update)
39
+ offset = update['update_id'] + 1
40
+
41
+ except CancelledError:
42
+ break
43
+ except exception.BadHTTPResponse as e:
44
+ traceback.print_exc()
45
+
46
+ # Servers probably down. Wait longer.
47
+ if e.status == 502:
48
+ await asyncio.sleep(30)
49
+ except:
50
+ traceback.print_exc()
51
+ await asyncio.sleep(relax)
52
+ else:
53
+ await asyncio.sleep(relax)
54
+
55
+
56
+ def _infer_handler_function(bot, h):
57
+ if h is None:
58
+ handler = bot.handle
59
+ elif isinstance(h, dict):
60
+ handler = flavor_router(h)
61
+ else:
62
+ handler = h
63
+
64
+ def create_task_for(msg):
65
+ bot.loop.create_task(handler(msg))
66
+
67
+ if asyncio.iscoroutinefunction(handler):
68
+ return create_task_for
69
+ else:
70
+ return handler
71
+
72
+
73
+ class MessageLoop(object):
74
+ def __init__(self, bot, handle=None):
75
+ self._bot = bot
76
+ self._handle = _infer_handler_function(bot, handle)
77
+ self._task = None
78
+
79
+ async def run_forever(self, *args, **kwargs):
80
+ updatesloop = GetUpdatesLoop(self._bot,
81
+ lambda update:
82
+ self._handle(_extract_message(update)[1]))
83
+
84
+ self._task = self._bot.loop.create_task(updatesloop.run_forever(*args, **kwargs))
85
+
86
+ self._bot.scheduler.on_event(self._handle)
87
+
88
+ def cancel(self):
89
+ self._task.cancel()
90
+
91
+
92
+ class Webhook(object):
93
+ def __init__(self, bot, handle=None):
94
+ self._bot = bot
95
+ self._handle = _infer_handler_function(bot, handle)
96
+
97
+ async def run_forever(self):
98
+ self._bot.scheduler.on_event(self._handle)
99
+
100
+ def feed(self, data):
101
+ update = _dictify(data)
102
+ self._handle(_extract_message(update)[1])
103
+
104
+
105
+ class OrderedWebhook(object):
106
+ def __init__(self, bot, handle=None):
107
+ self._bot = bot
108
+ self._handle = _infer_handler_function(bot, handle)
109
+ self._update_queue = asyncio.Queue(loop=bot.loop)
110
+
111
+ async def run_forever(self, maxhold=3):
112
+ self._bot.scheduler.on_event(self._handle)
113
+
114
+ def extract_handle(update):
115
+ try:
116
+ self._handle(_extract_message(update)[1])
117
+ except:
118
+ # Localize the error so message thread can keep going.
119
+ traceback.print_exc()
120
+ finally:
121
+ return update['update_id']
122
+
123
+ # Here is the re-ordering mechanism, ensuring in-order delivery of updates.
124
+ max_id = None # max update_id passed to callback
125
+ buffer = collections.deque() # keep those updates which skip some update_id
126
+ qwait = None # how long to wait for updates,
127
+ # because buffer's content has to be returned in time.
128
+
129
+ while 1:
130
+ try:
131
+ update = await asyncio.wait_for(self._update_queue.get(), qwait)
132
+
133
+ if max_id is None:
134
+ # First message received, handle regardless.
135
+ max_id = extract_handle(update)
136
+
137
+ elif update['update_id'] == max_id + 1:
138
+ # No update_id skipped, handle naturally.
139
+ max_id = extract_handle(update)
140
+
141
+ # clear contagious updates in buffer
142
+ if len(buffer) > 0:
143
+ buffer.popleft() # first element belongs to update just received, useless now.
144
+ while 1:
145
+ try:
146
+ if type(buffer[0]) is dict:
147
+ max_id = extract_handle(buffer.popleft()) # updates that arrived earlier, handle them.
148
+ else:
149
+ break # gap, no more contagious updates
150
+ except IndexError:
151
+ break # buffer empty
152
+
153
+ elif update['update_id'] > max_id + 1:
154
+ # Update arrives pre-maturely, insert to buffer.
155
+ nbuf = len(buffer)
156
+ if update['update_id'] <= max_id + nbuf:
157
+ # buffer long enough, put update at position
158
+ buffer[update['update_id'] - max_id - 1] = update
159
+ else:
160
+ # buffer too short, lengthen it
161
+ expire = time.time() + maxhold
162
+ for a in range(nbuf, update['update_id']-max_id-1):
163
+ buffer.append(expire) # put expiry time in gaps
164
+ buffer.append(update)
165
+
166
+ else:
167
+ pass # discard
168
+
169
+ except asyncio.TimeoutError:
170
+ # debug message
171
+ # print('Timeout')
172
+
173
+ # some buffer contents have to be handled
174
+ # flush buffer until a non-expired time is encountered
175
+ while 1:
176
+ try:
177
+ if type(buffer[0]) is dict:
178
+ max_id = extract_handle(buffer.popleft())
179
+ else:
180
+ expire = buffer[0]
181
+ if expire <= time.time():
182
+ max_id += 1
183
+ buffer.popleft()
184
+ else:
185
+ break # non-expired
186
+ except IndexError:
187
+ break # buffer empty
188
+ except:
189
+ traceback.print_exc()
190
+ finally:
191
+ try:
192
+ # don't wait longer than next expiry time
193
+ qwait = buffer[0] - time.time()
194
+ if qwait < 0:
195
+ qwait = 0
196
+ except IndexError:
197
+ # buffer empty, can wait forever
198
+ qwait = None
199
+
200
+ # debug message
201
+ # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
202
+
203
+ def feed(self, data):
204
+ update = _dictify(data)
205
+ self._update_queue.put_nowait(update)
telepot/aio/routing.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .helper import _create_invoker
2
+ from .. import all_content_types
3
+
4
+ # Mirror traditional version to avoid having to import one more module
5
+ from ..routing import (
6
+ by_content_type, by_command, by_chat_command, by_text, by_data, by_regex,
7
+ process_key, lower_key, upper_key
8
+ )
9
+
10
+ def make_routing_table(obj, keys, prefix='on_'):
11
+ """
12
+ :return:
13
+ a dictionary roughly equivalent to ``{'key1': obj.on_key1, 'key2': obj.on_key2, ...}``,
14
+ but ``obj`` does not have to define all methods. It may define the needed ones only.
15
+
16
+ :param obj: the object
17
+
18
+ :param keys: a list of keys
19
+
20
+ :param prefix: a string to be prepended to keys to make method names
21
+ """
22
+ def maptuple(k):
23
+ if isinstance(k, tuple):
24
+ if len(k) == 2:
25
+ return k
26
+ elif len(k) == 1:
27
+ return k[0], _create_invoker(obj, prefix+k[0])
28
+ else:
29
+ raise ValueError()
30
+ else:
31
+ return k, _create_invoker(obj, prefix+k)
32
+
33
+ return dict([maptuple(k) for k in keys])
34
+
35
+ def make_content_type_routing_table(obj, prefix='on_'):
36
+ """
37
+ :return:
38
+ a dictionary covering all available content types, roughly equivalent to
39
+ ``{'text': obj.on_text, 'photo': obj.on_photo, ...}``,
40
+ but ``obj`` does not have to define all methods. It may define the needed ones only.
41
+
42
+ :param obj: the object
43
+
44
+ :param prefix: a string to be prepended to content types to make method names
45
+ """
46
+ return make_routing_table(obj, all_content_types, prefix)
telepot/api.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import urllib3
2
+ import logging
3
+ import json
4
+ import re
5
+ import os
6
+
7
+ from . import exception, _isstring
8
+
9
+ # Suppress InsecurePlatformWarning
10
+ urllib3.disable_warnings()
11
+
12
+
13
+ _default_pool_params = dict(num_pools=3, maxsize=10, retries=3, timeout=30)
14
+ _onetime_pool_params = dict(num_pools=1, maxsize=1, retries=3, timeout=30)
15
+
16
+ _pools = {
17
+ 'default': urllib3.PoolManager(**_default_pool_params),
18
+ }
19
+
20
+ _onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params)
21
+
22
+
23
+ def set_proxy(url, basic_auth=None):
24
+ """
25
+ Access Bot API through a proxy.
26
+
27
+ :param url: proxy URL
28
+ :param basic_auth: 2-tuple ``('username', 'password')``
29
+ """
30
+ global _pools, _onetime_pool_spec
31
+ if not url:
32
+ _pools['default'] = urllib3.PoolManager(**_default_pool_params)
33
+ _onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params)
34
+ elif basic_auth:
35
+ h = urllib3.make_headers(proxy_basic_auth=':'.join(basic_auth))
36
+ _pools['default'] = urllib3.ProxyManager(url, proxy_headers=h, **_default_pool_params)
37
+ _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, proxy_headers=h, **_onetime_pool_params))
38
+ else:
39
+ _pools['default'] = urllib3.ProxyManager(url, **_default_pool_params)
40
+ _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, **_onetime_pool_params))
41
+
42
+ def _create_onetime_pool():
43
+ cls, kw = _onetime_pool_spec
44
+ return cls(**kw)
45
+
46
+ def _methodurl(req, **user_kw):
47
+ token, method, params, files = req
48
+ return 'https://api.telegram.org/bot%s/%s' % (token, method)
49
+
50
+ def _which_pool(req, **user_kw):
51
+ token, method, params, files = req
52
+ return None if files else 'default'
53
+
54
+ def _guess_filename(obj):
55
+ name = getattr(obj, 'name', None)
56
+ if name and _isstring(name) and name[0] != '<' and name[-1] != '>':
57
+ return os.path.basename(name)
58
+
59
+ def _filetuple(key, f):
60
+ if not isinstance(f, tuple):
61
+ return (_guess_filename(f) or key, f.read())
62
+ elif len(f) == 1:
63
+ return (_guess_filename(f[0]) or key, f[0].read())
64
+ elif len(f) == 2:
65
+ return (f[0], f[1].read())
66
+ elif len(f) == 3:
67
+ return (f[0], f[1].read(), f[2])
68
+ else:
69
+ raise ValueError()
70
+
71
+ import sys
72
+ PY_3 = sys.version_info.major >= 3
73
+ def _fix_type(v):
74
+ if isinstance(v, float if PY_3 else (long, float)):
75
+ return str(v)
76
+ else:
77
+ return v
78
+
79
+ def _compose_fields(req, **user_kw):
80
+ token, method, params, files = req
81
+
82
+ fields = {k:_fix_type(v) for k,v in params.items()} if params is not None else {}
83
+ if files:
84
+ fields.update({k:_filetuple(k,v) for k,v in files.items()})
85
+
86
+ return fields
87
+
88
+ def _default_timeout(req, **user_kw):
89
+ name = _which_pool(req, **user_kw)
90
+ if name is None:
91
+ return _onetime_pool_spec[1]['timeout']
92
+ else:
93
+ return _pools[name].connection_pool_kw['timeout']
94
+
95
+ def _compose_kwargs(req, **user_kw):
96
+ token, method, params, files = req
97
+ kw = {}
98
+
99
+ if not params and not files:
100
+ kw['encode_multipart'] = False
101
+
102
+ if method == 'getUpdates' and params and 'timeout' in params:
103
+ # Ensure HTTP timeout is longer than getUpdates timeout
104
+ kw['timeout'] = params['timeout'] + _default_timeout(req, **user_kw)
105
+ elif files:
106
+ # Disable timeout if uploading files. For some reason, the larger the file,
107
+ # the longer it takes for the server to respond (after upload is finished).
108
+ # It is unclear how long timeout should be.
109
+ kw['timeout'] = None
110
+
111
+ # Let user-supplied arguments override
112
+ kw.update(user_kw)
113
+ return kw
114
+
115
+ def _transform(req, **user_kw):
116
+ kwargs = _compose_kwargs(req, **user_kw)
117
+
118
+ fields = _compose_fields(req, **user_kw)
119
+
120
+ url = _methodurl(req, **user_kw)
121
+
122
+ name = _which_pool(req, **user_kw)
123
+
124
+ if name is None:
125
+ pool = _create_onetime_pool()
126
+ else:
127
+ pool = _pools[name]
128
+
129
+ return pool.request_encode_body, ('POST', url, fields), kwargs
130
+
131
+ def _parse(response):
132
+ try:
133
+ text = response.data.decode('utf-8')
134
+ data = json.loads(text)
135
+ except ValueError: # No JSON object could be decoded
136
+ raise exception.BadHTTPResponse(response.status, text, response)
137
+
138
+ if data['ok']:
139
+ return data['result']
140
+ else:
141
+ description, error_code = data['description'], data['error_code']
142
+
143
+ # Look for specific error ...
144
+ for e in exception.TelegramError.__subclasses__():
145
+ n = len(e.DESCRIPTION_PATTERNS)
146
+ if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])):
147
+ raise e(description, error_code, data)
148
+
149
+ # ... or raise generic error
150
+ raise exception.TelegramError(description, error_code, data)
151
+
152
+ def request(req, **user_kw):
153
+ fn, args, kwargs = _transform(req, **user_kw)
154
+ r = fn(*args, **kwargs) # `fn` must be thread-safe
155
+ return _parse(r)
156
+
157
+ def _fileurl(req):
158
+ token, path = req
159
+ return 'https://api.telegram.org/file/bot%s/%s' % (token, path)
160
+
161
+ def download(req, **user_kw):
162
+ pool = _create_onetime_pool()
163
+ r = pool.request('GET', _fileurl(req), **user_kw)
164
+ return r
telepot/delegate.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import traceback
2
+ from functools import wraps
3
+ from . import exception
4
+ from . import flavor, peel, is_event, chat_flavors, inline_flavors
5
+
6
+ def _wrap_none(fn):
7
+ def w(*args, **kwargs):
8
+ try:
9
+ return fn(*args, **kwargs)
10
+ except (KeyError, exception.BadFlavor):
11
+ return None
12
+ return w
13
+
14
+ def per_chat_id(types='all'):
15
+ """
16
+ :param types:
17
+ ``all`` or a list of chat types (``private``, ``group``, ``channel``)
18
+
19
+ :return:
20
+ a seeder function that returns the chat id only if the chat type is in ``types``.
21
+ """
22
+ return _wrap_none(lambda msg:
23
+ msg['chat']['id']
24
+ if types == 'all' or msg['chat']['type'] in types
25
+ else None)
26
+
27
+ def per_chat_id_in(s, types='all'):
28
+ """
29
+ :param s:
30
+ a list or set of chat id
31
+
32
+ :param types:
33
+ ``all`` or a list of chat types (``private``, ``group``, ``channel``)
34
+
35
+ :return:
36
+ a seeder function that returns the chat id only if the chat id is in ``s``
37
+ and chat type is in ``types``.
38
+ """
39
+ return _wrap_none(lambda msg:
40
+ msg['chat']['id']
41
+ if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] in s
42
+ else None)
43
+
44
+ def per_chat_id_except(s, types='all'):
45
+ """
46
+ :param s:
47
+ a list or set of chat id
48
+
49
+ :param types:
50
+ ``all`` or a list of chat types (``private``, ``group``, ``channel``)
51
+
52
+ :return:
53
+ a seeder function that returns the chat id only if the chat id is *not* in ``s``
54
+ and chat type is in ``types``.
55
+ """
56
+ return _wrap_none(lambda msg:
57
+ msg['chat']['id']
58
+ if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] not in s
59
+ else None)
60
+
61
+ def per_from_id(flavors=chat_flavors+inline_flavors):
62
+ """
63
+ :param flavors:
64
+ ``all`` or a list of flavors
65
+
66
+ :return:
67
+ a seeder function that returns the from id only if the message flavor is
68
+ in ``flavors``.
69
+ """
70
+ return _wrap_none(lambda msg:
71
+ msg['from']['id']
72
+ if flavors == 'all' or flavor(msg) in flavors
73
+ else None)
74
+
75
+ def per_from_id_in(s, flavors=chat_flavors+inline_flavors):
76
+ """
77
+ :param s:
78
+ a list or set of from id
79
+
80
+ :param flavors:
81
+ ``all`` or a list of flavors
82
+
83
+ :return:
84
+ a seeder function that returns the from id only if the from id is in ``s``
85
+ and message flavor is in ``flavors``.
86
+ """
87
+ return _wrap_none(lambda msg:
88
+ msg['from']['id']
89
+ if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] in s
90
+ else None)
91
+
92
+ def per_from_id_except(s, flavors=chat_flavors+inline_flavors):
93
+ """
94
+ :param s:
95
+ a list or set of from id
96
+
97
+ :param flavors:
98
+ ``all`` or a list of flavors
99
+
100
+ :return:
101
+ a seeder function that returns the from id only if the from id is *not* in ``s``
102
+ and message flavor is in ``flavors``.
103
+ """
104
+ return _wrap_none(lambda msg:
105
+ msg['from']['id']
106
+ if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] not in s
107
+ else None)
108
+
109
+ def per_inline_from_id():
110
+ """
111
+ :return:
112
+ a seeder function that returns the from id only if the message flavor
113
+ is ``inline_query`` or ``chosen_inline_result``
114
+ """
115
+ return per_from_id(flavors=inline_flavors)
116
+
117
+ def per_inline_from_id_in(s):
118
+ """
119
+ :param s: a list or set of from id
120
+ :return:
121
+ a seeder function that returns the from id only if the message flavor
122
+ is ``inline_query`` or ``chosen_inline_result`` and the from id is in ``s``.
123
+ """
124
+ return per_from_id_in(s, flavors=inline_flavors)
125
+
126
+ def per_inline_from_id_except(s):
127
+ """
128
+ :param s: a list or set of from id
129
+ :return:
130
+ a seeder function that returns the from id only if the message flavor
131
+ is ``inline_query`` or ``chosen_inline_result`` and the from id is *not* in ``s``.
132
+ """
133
+ return per_from_id_except(s, flavors=inline_flavors)
134
+
135
+ def per_application():
136
+ """
137
+ :return:
138
+ a seeder function that always returns 1, ensuring at most one delegate is ever spawned
139
+ for the entire application.
140
+ """
141
+ return lambda msg: 1
142
+
143
+ def per_message(flavors='all'):
144
+ """
145
+ :param flavors: ``all`` or a list of flavors
146
+ :return:
147
+ a seeder function that returns a non-hashable only if the message flavor
148
+ is in ``flavors``.
149
+ """
150
+ return _wrap_none(lambda msg: [] if flavors == 'all' or flavor(msg) in flavors else None)
151
+
152
+ def per_event_source_id(event_space):
153
+ """
154
+ :return:
155
+ a seeder function that returns an event's source id only if that event's
156
+ source space equals to ``event_space``.
157
+ """
158
+ def f(event):
159
+ if is_event(event):
160
+ v = peel(event)
161
+ if v['source']['space'] == event_space:
162
+ return v['source']['id']
163
+ else:
164
+ return None
165
+ else:
166
+ return None
167
+ return _wrap_none(f)
168
+
169
+ def per_callback_query_chat_id(types='all'):
170
+ """
171
+ :param types:
172
+ ``all`` or a list of chat types (``private``, ``group``, ``channel``)
173
+
174
+ :return:
175
+ a seeder function that returns a callback query's originating chat id
176
+ if the chat type is in ``types``.
177
+ """
178
+ def f(msg):
179
+ if (flavor(msg) == 'callback_query' and 'message' in msg
180
+ and (types == 'all' or msg['message']['chat']['type'] in types)):
181
+ return msg['message']['chat']['id']
182
+ else:
183
+ return None
184
+ return f
185
+
186
+ def per_callback_query_origin(origins='all'):
187
+ """
188
+ :param origins:
189
+ ``all`` or a list of origin types (``chat``, ``inline``)
190
+
191
+ :return:
192
+ a seeder function that returns a callback query's origin identifier if
193
+ that origin type is in ``origins``. The origin identifier is guaranteed
194
+ to be a tuple.
195
+ """
196
+ def f(msg):
197
+ def origin_type_ok():
198
+ return (origins == 'all'
199
+ or ('chat' in origins and 'message' in msg)
200
+ or ('inline' in origins and 'inline_message_id' in msg))
201
+
202
+ if flavor(msg) == 'callback_query' and origin_type_ok():
203
+ if 'inline_message_id' in msg:
204
+ return msg['inline_message_id'],
205
+ else:
206
+ return msg['message']['chat']['id'], msg['message']['message_id']
207
+ else:
208
+ return None
209
+ return f
210
+
211
+ def per_invoice_payload():
212
+ """
213
+ :return:
214
+ a seeder function that returns the invoice payload.
215
+ """
216
+ def f(msg):
217
+ if 'successful_payment' in msg:
218
+ return msg['successful_payment']['invoice_payload']
219
+ else:
220
+ return msg['invoice_payload']
221
+
222
+ return _wrap_none(f)
223
+
224
+ def call(func, *args, **kwargs):
225
+ """
226
+ :return:
227
+ a delegator function that returns a tuple (``func``, (seed tuple,)+ ``args``, ``kwargs``).
228
+ That is, seed tuple is inserted before supplied positional arguments.
229
+ By default, a thread wrapping ``func`` and all those arguments is spawned.
230
+ """
231
+ def f(seed_tuple):
232
+ return func, (seed_tuple,)+args, kwargs
233
+ return f
234
+
235
+ def create_run(cls, *args, **kwargs):
236
+ """
237
+ :return:
238
+ a delegator function that calls the ``cls`` constructor whose arguments being
239
+ a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
240
+ the object's ``run`` method. By default, a thread wrapping that ``run`` method
241
+ is spawned.
242
+ """
243
+ def f(seed_tuple):
244
+ j = cls(seed_tuple, *args, **kwargs)
245
+ return j.run
246
+ return f
247
+
248
+ def create_open(cls, *args, **kwargs):
249
+ """
250
+ :return:
251
+ a delegator function that calls the ``cls`` constructor whose arguments being
252
+ a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
253
+ a looping function that uses the object's ``listener`` to wait for messages
254
+ and invokes instance method ``open``, ``on_message``, and ``on_close`` accordingly.
255
+ By default, a thread wrapping that looping function is spawned.
256
+ """
257
+ def f(seed_tuple):
258
+ j = cls(seed_tuple, *args, **kwargs)
259
+
260
+ def wait_loop():
261
+ bot, msg, seed = seed_tuple
262
+ try:
263
+ handled = j.open(msg, seed)
264
+ if not handled:
265
+ j.on_message(msg)
266
+
267
+ while 1:
268
+ msg = j.listener.wait()
269
+ j.on_message(msg)
270
+
271
+ # These exceptions are "normal" exits.
272
+ except (exception.IdleTerminate, exception.StopListening) as e:
273
+ j.on_close(e)
274
+
275
+ # Any other exceptions are accidents. **Print it out.**
276
+ # This is to prevent swallowing exceptions in the case that on_close()
277
+ # gets overridden but fails to account for unexpected exceptions.
278
+ except Exception as e:
279
+ traceback.print_exc()
280
+ j.on_close(e)
281
+
282
+ return wait_loop
283
+ return f
284
+
285
+ def until(condition, fns):
286
+ """
287
+ Try a list of seeder functions until a condition is met.
288
+
289
+ :param condition:
290
+ a function that takes one argument - a seed - and returns ``True``
291
+ or ``False``
292
+
293
+ :param fns:
294
+ a list of seeder functions
295
+
296
+ :return:
297
+ a "composite" seeder function that calls each supplied function in turn,
298
+ and returns the first seed where the condition is met. If the condition
299
+ is never met, it returns ``None``.
300
+ """
301
+ def f(msg):
302
+ for fn in fns:
303
+ seed = fn(msg)
304
+ if condition(seed):
305
+ return seed
306
+ return None
307
+ return f
308
+
309
+ def chain(*fns):
310
+ """
311
+ :return:
312
+ a "composite" seeder function that calls each supplied function in turn,
313
+ and returns the first seed that is not ``None``.
314
+ """
315
+ return until(lambda seed: seed is not None, fns)
316
+
317
+ def _ensure_seeders_list(fn):
318
+ @wraps(fn)
319
+ def e(seeders, *aa, **kw):
320
+ return fn(seeders if isinstance(seeders, list) else [seeders], *aa, **kw)
321
+ return e
322
+
323
+ @_ensure_seeders_list
324
+ def pair(seeders, delegator_factory, *args, **kwargs):
325
+ """
326
+ The basic pair producer.
327
+
328
+ :return:
329
+ a (seeder, delegator_factory(\*args, \*\*kwargs)) tuple.
330
+
331
+ :param seeders:
332
+ If it is a seeder function or a list of one seeder function, it is returned
333
+ as the final seeder. If it is a list of more than one seeder function, they
334
+ are chained together before returned as the final seeder.
335
+ """
336
+ return (chain(*seeders) if len(seeders) > 1 else seeders[0],
337
+ delegator_factory(*args, **kwargs))
338
+
339
+ def _natural_numbers():
340
+ x = 0
341
+ while 1:
342
+ x += 1
343
+ yield x
344
+
345
+ _event_space = _natural_numbers()
346
+
347
+ def pave_event_space(fn=pair):
348
+ """
349
+ :return:
350
+ a pair producer that ensures the seeder and delegator share the same event space.
351
+ """
352
+ global _event_space
353
+ event_space = next(_event_space)
354
+
355
+ @_ensure_seeders_list
356
+ def p(seeders, delegator_factory, *args, **kwargs):
357
+ return fn(seeders + [per_event_source_id(event_space)],
358
+ delegator_factory, *args, event_space=event_space, **kwargs)
359
+ return p
360
+
361
+ def include_callback_query_chat_id(fn=pair, types='all'):
362
+ """
363
+ :return:
364
+ a pair producer that enables static callback query capturing
365
+ across seeder and delegator.
366
+
367
+ :param types:
368
+ ``all`` or a list of chat types (``private``, ``group``, ``channel``)
369
+ """
370
+ @_ensure_seeders_list
371
+ def p(seeders, delegator_factory, *args, **kwargs):
372
+ return fn(seeders + [per_callback_query_chat_id(types=types)],
373
+ delegator_factory, *args, include_callback_query=True, **kwargs)
374
+ return p
375
+
376
+ from . import helper
377
+
378
+ def intercept_callback_query_origin(fn=pair, origins='all'):
379
+ """
380
+ :return:
381
+ a pair producer that enables dynamic callback query origin mapping
382
+ across seeder and delegator.
383
+
384
+ :param origins:
385
+ ``all`` or a list of origin types (``chat``, ``inline``).
386
+ Origin mapping is only enabled for specified origin types.
387
+ """
388
+ origin_map = helper.SafeDict()
389
+
390
+ # For key functions that returns a tuple as key (e.g. per_callback_query_origin()),
391
+ # wrap the key in another tuple to prevent router from mistaking it as
392
+ # a key followed by some arguments.
393
+ def tuplize(fn):
394
+ def tp(msg):
395
+ return (fn(msg),)
396
+ return tp
397
+
398
+ router = helper.Router(tuplize(per_callback_query_origin(origins=origins)),
399
+ origin_map)
400
+
401
+ def modify_origin_map(origin, dest, set):
402
+ if set:
403
+ origin_map[origin] = dest
404
+ else:
405
+ try:
406
+ del origin_map[origin]
407
+ except KeyError:
408
+ pass
409
+
410
+ if origins == 'all':
411
+ intercept = modify_origin_map
412
+ else:
413
+ intercept = (modify_origin_map if 'chat' in origins else False,
414
+ modify_origin_map if 'inline' in origins else False)
415
+
416
+ @_ensure_seeders_list
417
+ def p(seeders, delegator_factory, *args, **kwargs):
418
+ return fn(seeders + [_wrap_none(router.map)],
419
+ delegator_factory, *args, intercept_callback_query=intercept, **kwargs)
420
+ return p
telepot/exception.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ class TelepotException(Exception):
4
+ """ Base class of following exceptions. """
5
+ pass
6
+
7
+ class BadFlavor(TelepotException):
8
+ def __init__(self, offender):
9
+ super(BadFlavor, self).__init__(offender)
10
+
11
+ @property
12
+ def offender(self):
13
+ return self.args[0]
14
+
15
+ PY_3 = sys.version_info.major >= 3
16
+
17
+ class BadHTTPResponse(TelepotException):
18
+ """
19
+ All requests to Bot API should result in a JSON response. If non-JSON, this
20
+ exception is raised. While it is hard to pinpoint exactly when this might happen,
21
+ the following situations have been observed to give rise to it:
22
+
23
+ - an unreasonable token, e.g. ``abc``, ``123``, anything that does not even
24
+ remotely resemble a correct token.
25
+ - a bad gateway, e.g. when Telegram servers are down.
26
+ """
27
+
28
+ def __init__(self, status, text, response):
29
+ super(BadHTTPResponse, self).__init__(status, text, response)
30
+
31
+ @property
32
+ def status(self):
33
+ return self.args[0]
34
+
35
+ @property
36
+ def text(self):
37
+ return self.args[1]
38
+
39
+ @property
40
+ def response(self):
41
+ return self.args[2]
42
+
43
+ class EventNotFound(TelepotException):
44
+ def __init__(self, event):
45
+ super(EventNotFound, self).__init__(event)
46
+
47
+ @property
48
+ def event(self):
49
+ return self.args[0]
50
+
51
+ class WaitTooLong(TelepotException):
52
+ def __init__(self, seconds):
53
+ super(WaitTooLong, self).__init__(seconds)
54
+
55
+ @property
56
+ def seconds(self):
57
+ return self.args[0]
58
+
59
+ class IdleTerminate(WaitTooLong):
60
+ pass
61
+
62
+ class StopListening(TelepotException):
63
+ pass
64
+
65
+ class TelegramError(TelepotException):
66
+ """
67
+ To indicate erroneous situations, Telegram returns a JSON object containing
68
+ an *error code* and a *description*. This will cause a ``TelegramError`` to
69
+ be raised. Before raising a generic ``TelegramError``, telepot looks for
70
+ a more specific subclass that "matches" the error. If such a class exists,
71
+ an exception of that specific subclass is raised. This allows you to either
72
+ catch specific errors or to cast a wide net (by a catch-all ``TelegramError``).
73
+ This also allows you to incorporate custom ``TelegramError`` easily.
74
+
75
+ Subclasses must define a class variable ``DESCRIPTION_PATTERNS`` which is a list
76
+ of regular expressions. If an error's *description* matches any of the regular expressions,
77
+ an exception of that subclass is raised.
78
+ """
79
+
80
+ def __init__(self, description, error_code, json):
81
+ super(TelegramError, self).__init__(description, error_code, json)
82
+
83
+ @property
84
+ def description(self):
85
+ return self.args[0]
86
+
87
+ @property
88
+ def error_code(self):
89
+ return self.args[1]
90
+
91
+ @property
92
+ def json(self):
93
+ return self.args[2]
94
+
95
+ class UnauthorizedError(TelegramError):
96
+ DESCRIPTION_PATTERNS = ['unauthorized']
97
+
98
+ class BotWasKickedError(TelegramError):
99
+ DESCRIPTION_PATTERNS = ['bot.*kicked']
100
+
101
+ class BotWasBlockedError(TelegramError):
102
+ DESCRIPTION_PATTERNS = ['bot.*blocked']
103
+
104
+ class TooManyRequestsError(TelegramError):
105
+ DESCRIPTION_PATTERNS = ['too *many *requests']
106
+
107
+ class MigratedToSupergroupChatError(TelegramError):
108
+ DESCRIPTION_PATTERNS = ['migrated.*supergroup *chat']
109
+
110
+ class NotEnoughRightsError(TelegramError):
111
+ DESCRIPTION_PATTERNS = ['not *enough *rights']
telepot/filtering.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def pick(obj, keys):
2
+ def pick1(k):
3
+ if type(obj) is dict:
4
+ return obj[k]
5
+ else:
6
+ return getattr(obj, k)
7
+
8
+ if isinstance(keys, list):
9
+ return [pick1(k) for k in keys]
10
+ else:
11
+ return pick1(keys)
12
+
13
+ def match(data, template):
14
+ if isinstance(template, dict) and isinstance(data, dict):
15
+ def pick_and_match(kv):
16
+ template_key, template_value = kv
17
+ if hasattr(template_key, 'search'): # regex
18
+ data_keys = list(filter(template_key.search, data.keys()))
19
+ if not data_keys:
20
+ return False
21
+ elif template_key in data:
22
+ data_keys = [template_key]
23
+ else:
24
+ return False
25
+ return any(map(lambda data_value: match(data_value, template_value), pick(data, data_keys)))
26
+
27
+ return all(map(pick_and_match, template.items()))
28
+ elif callable(template):
29
+ return template(data)
30
+ else:
31
+ return data == template
32
+
33
+ def match_all(msg, templates):
34
+ return all(map(lambda t: match(msg, t), templates))
telepot/hack.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ import urllib3.fields
3
+
4
+ # Do not encode unicode filename, so Telegram servers understand it.
5
+ def _noencode_filename(fn):
6
+ def w(name, value):
7
+ if name == 'filename':
8
+ return '%s="%s"' % (name, value)
9
+ else:
10
+ return fn(name, value)
11
+ return w
12
+
13
+ urllib3.fields.format_header_param = _noencode_filename(urllib3.fields.format_header_param)
14
+
15
+ except (ImportError, AttributeError):
16
+ pass
telepot/helper.py ADDED
@@ -0,0 +1,1170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import traceback
3
+ import threading
4
+ import logging
5
+ import collections
6
+ import re
7
+ import inspect
8
+ from functools import partial
9
+ from . import filtering, exception
10
+ from . import (
11
+ flavor, chat_flavors, inline_flavors, is_event,
12
+ message_identifier, origin_identifier)
13
+
14
+ try:
15
+ import Queue as queue
16
+ except ImportError:
17
+ import queue
18
+
19
+
20
+ class 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
+
48
+ class 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
+
91
+ class 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
+
139
+ class 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
+
192
+ class 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
+
231
+ class 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
+
314
+ class 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
+
329
+ class 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
+
565
+ class 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
+
600
+ class 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
+
653
+ class 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
+
710
+ class 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
+
736
+ class 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
+
819
+ class 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
+
835
+ class 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
+
860
+ class 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
+
882
+ class 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
+
898
+ class 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
+
915
+ class 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
+
925
+ def 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
+
970
+ class 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
+
1044
+ class 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
1071
+ class 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
1087
+ class 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
1106
+ class 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
+
1131
+ class 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
1140
+ class 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
1158
+ class 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}}])
telepot/loop.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import time
3
+ import json
4
+ import threading
5
+ import traceback
6
+ import collections
7
+
8
+ try:
9
+ import Queue as queue
10
+ except ImportError:
11
+ import queue
12
+
13
+ from . import exception
14
+ from . import _find_first_key, flavor_router
15
+
16
+
17
+ class RunForeverAsThread(object):
18
+ def run_as_thread(self, *args, **kwargs):
19
+ t = threading.Thread(target=self.run_forever, args=args, kwargs=kwargs)
20
+ t.daemon = True
21
+ t.start()
22
+
23
+
24
+ class CollectLoop(RunForeverAsThread):
25
+ def __init__(self, handle):
26
+ self._handle = handle
27
+ self._inqueue = queue.Queue()
28
+
29
+ @property
30
+ def input_queue(self):
31
+ return self._inqueue
32
+
33
+ def run_forever(self):
34
+ while 1:
35
+ try:
36
+ msg = self._inqueue.get(block=True)
37
+ self._handle(msg)
38
+ except:
39
+ traceback.print_exc()
40
+
41
+
42
+ class GetUpdatesLoop(RunForeverAsThread):
43
+ def __init__(self, bot, on_update):
44
+ self._bot = bot
45
+ self._update_handler = on_update
46
+
47
+ def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None):
48
+ """
49
+ Process new updates in infinity loop
50
+
51
+ :param relax: float
52
+ :param offset: int
53
+ :param timeout: int
54
+ :param allowed_updates: bool
55
+ """
56
+ while 1:
57
+ try:
58
+ result = self._bot.getUpdates(offset=offset,
59
+ timeout=timeout,
60
+ allowed_updates=allowed_updates)
61
+
62
+ # Once passed, this parameter is no longer needed.
63
+ allowed_updates = None
64
+
65
+ # No sort. Trust server to give messages in correct order.
66
+ for update in result:
67
+ self._update_handler(update)
68
+ offset = update['update_id'] + 1
69
+
70
+ except exception.BadHTTPResponse as e:
71
+ traceback.print_exc()
72
+
73
+ # Servers probably down. Wait longer.
74
+ if e.status == 502:
75
+ time.sleep(30)
76
+ except:
77
+ traceback.print_exc()
78
+ finally:
79
+ time.sleep(relax)
80
+
81
+
82
+ def _dictify3(data):
83
+ if type(data) is bytes:
84
+ return json.loads(data.decode('utf-8'))
85
+ elif type(data) is str:
86
+ return json.loads(data)
87
+ elif type(data) is dict:
88
+ return data
89
+ else:
90
+ raise ValueError()
91
+
92
+ def _dictify27(data):
93
+ if type(data) in [str, unicode]:
94
+ return json.loads(data)
95
+ elif type(data) is dict:
96
+ return data
97
+ else:
98
+ raise ValueError()
99
+
100
+ _dictify = _dictify3 if sys.version_info >= (3,) else _dictify27
101
+
102
+ def _extract_message(update):
103
+ key = _find_first_key(update, ['message',
104
+ 'edited_message',
105
+ 'channel_post',
106
+ 'edited_channel_post',
107
+ 'callback_query',
108
+ 'inline_query',
109
+ 'chosen_inline_result',
110
+ 'shipping_query',
111
+ 'pre_checkout_query'])
112
+ return key, update[key]
113
+
114
+ def _infer_handler_function(bot, h):
115
+ if h is None:
116
+ return bot.handle
117
+ elif isinstance(h, dict):
118
+ return flavor_router(h)
119
+ else:
120
+ return h
121
+
122
+
123
+ class MessageLoop(RunForeverAsThread):
124
+ def __init__(self, bot, handle=None):
125
+ self._bot = bot
126
+ self._handle = _infer_handler_function(bot, handle)
127
+
128
+ def run_forever(self, *args, **kwargs):
129
+ """
130
+ :type relax: float
131
+ :param relax: seconds between each :meth:`.getUpdates`
132
+
133
+ :type offset: int
134
+ :param offset:
135
+ initial ``offset`` parameter supplied to :meth:`.getUpdates`
136
+
137
+ :type timeout: int
138
+ :param timeout:
139
+ ``timeout`` parameter supplied to :meth:`.getUpdates`, controlling
140
+ how long to poll.
141
+
142
+ :type allowed_updates: array of string
143
+ :param allowed_updates:
144
+ ``allowed_updates`` parameter supplied to :meth:`.getUpdates`,
145
+ controlling which types of updates to receive.
146
+
147
+ Calling this method will block forever. Use :meth:`.run_as_thread` to
148
+ run it non-blockingly.
149
+ """
150
+ collectloop = CollectLoop(self._handle)
151
+ updatesloop = GetUpdatesLoop(self._bot,
152
+ lambda update:
153
+ collectloop.input_queue.put(_extract_message(update)[1]))
154
+ # feed messages to collect loop
155
+ # feed events to collect loop
156
+ self._bot.scheduler.on_event(collectloop.input_queue.put)
157
+ self._bot.scheduler.run_as_thread()
158
+
159
+ updatesloop.run_as_thread(*args, **kwargs)
160
+ collectloop.run_forever() # blocking
161
+
162
+
163
+ class Webhook(RunForeverAsThread):
164
+ def __init__(self, bot, handle=None):
165
+ self._bot = bot
166
+ self._collectloop = CollectLoop(_infer_handler_function(bot, handle))
167
+
168
+ def run_forever(self):
169
+ # feed events to collect loop
170
+ self._bot.scheduler.on_event(self._collectloop.input_queue.put)
171
+ self._bot.scheduler.run_as_thread()
172
+
173
+ self._collectloop.run_forever()
174
+
175
+ def feed(self, data):
176
+ update = _dictify(data)
177
+ self._collectloop.input_queue.put(_extract_message(update)[1])
178
+
179
+
180
+ class Orderer(RunForeverAsThread):
181
+ def __init__(self, on_ordered_update):
182
+ self._on_ordered_update = on_ordered_update
183
+ self._inqueue = queue.Queue()
184
+
185
+ @property
186
+ def input_queue(self):
187
+ return self._inqueue
188
+
189
+ def run_forever(self, maxhold=3):
190
+ def handle(update):
191
+ self._on_ordered_update(update)
192
+ return update['update_id']
193
+
194
+ # Here is the re-ordering mechanism, ensuring in-order delivery of updates.
195
+ max_id = None # max update_id passed to callback
196
+ buffer = collections.deque() # keep those updates which skip some update_id
197
+ qwait = None # how long to wait for updates,
198
+ # because buffer's content has to be returned in time.
199
+
200
+ while 1:
201
+ try:
202
+ update = self._inqueue.get(block=True, timeout=qwait)
203
+
204
+ if max_id is None:
205
+ # First message received, handle regardless.
206
+ max_id = handle(update)
207
+
208
+ elif update['update_id'] == max_id + 1:
209
+ # No update_id skipped, handle naturally.
210
+ max_id = handle(update)
211
+
212
+ # clear contagious updates in buffer
213
+ if len(buffer) > 0:
214
+ buffer.popleft() # first element belongs to update just received, useless now.
215
+ while 1:
216
+ try:
217
+ if type(buffer[0]) is dict:
218
+ max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them.
219
+ else:
220
+ break # gap, no more contagious updates
221
+ except IndexError:
222
+ break # buffer empty
223
+
224
+ elif update['update_id'] > max_id + 1:
225
+ # Update arrives pre-maturely, insert to buffer.
226
+ nbuf = len(buffer)
227
+ if update['update_id'] <= max_id + nbuf:
228
+ # buffer long enough, put update at position
229
+ buffer[update['update_id'] - max_id - 1] = update
230
+ else:
231
+ # buffer too short, lengthen it
232
+ expire = time.time() + maxhold
233
+ for a in range(nbuf, update['update_id']-max_id-1):
234
+ buffer.append(expire) # put expiry time in gaps
235
+ buffer.append(update)
236
+
237
+ else:
238
+ pass # discard
239
+
240
+ except queue.Empty:
241
+ # debug message
242
+ # print('Timeout')
243
+
244
+ # some buffer contents have to be handled
245
+ # flush buffer until a non-expired time is encountered
246
+ while 1:
247
+ try:
248
+ if type(buffer[0]) is dict:
249
+ max_id = handle(buffer.popleft())
250
+ else:
251
+ expire = buffer[0]
252
+ if expire <= time.time():
253
+ max_id += 1
254
+ buffer.popleft()
255
+ else:
256
+ break # non-expired
257
+ except IndexError:
258
+ break # buffer empty
259
+ except:
260
+ traceback.print_exc()
261
+ finally:
262
+ try:
263
+ # don't wait longer than next expiry time
264
+ qwait = buffer[0] - time.time()
265
+ if qwait < 0:
266
+ qwait = 0
267
+ except IndexError:
268
+ # buffer empty, can wait forever
269
+ qwait = None
270
+
271
+ # debug message
272
+ # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id)
273
+
274
+
275
+ class OrderedWebhook(RunForeverAsThread):
276
+ def __init__(self, bot, handle=None):
277
+ self._bot = bot
278
+ self._collectloop = CollectLoop(_infer_handler_function(bot, handle))
279
+ self._orderer = Orderer(lambda update:
280
+ self._collectloop.input_queue.put(_extract_message(update)[1]))
281
+ # feed messages to collect loop
282
+
283
+ def run_forever(self, *args, **kwargs):
284
+ """
285
+ :type maxhold: float
286
+ :param maxhold:
287
+ The maximum number of seconds an update is held waiting for a
288
+ not-yet-arrived smaller ``update_id``. When this number of seconds
289
+ is up, the update is delivered to the message-handling function
290
+ even if some smaller ``update_id``\s have not yet arrived. If those
291
+ smaller ``update_id``\s arrive at some later time, they are discarded.
292
+
293
+ Calling this method will block forever. Use :meth:`.run_as_thread` to
294
+ run it non-blockingly.
295
+ """
296
+ # feed events to collect loop
297
+ self._bot.scheduler.on_event(self._collectloop.input_queue.put)
298
+ self._bot.scheduler.run_as_thread()
299
+
300
+ self._orderer.run_as_thread(*args, **kwargs)
301
+ self._collectloop.run_forever()
302
+
303
+ def feed(self, data):
304
+ """
305
+ :param data:
306
+ One of these:
307
+
308
+ - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8)
309
+ representing a JSON-serialized `Update <https://core.telegram.org/bots/api#update>`_ object.
310
+ - a ``dict`` representing an Update object.
311
+ """
312
+ update = _dictify(data)
313
+ self._orderer.input_queue.put(update)
telepot/namedtuple.py ADDED
@@ -0,0 +1,865 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collections
2
+ import warnings
3
+ import sys
4
+
5
+ class _Field(object):
6
+ def __init__(self, name, constructor=None, default=None):
7
+ self.name = name
8
+ self.constructor = constructor
9
+ self.default = default
10
+
11
+ # Function to produce namedtuple classes.
12
+ def _create_class(typename, fields):
13
+ # extract field names
14
+ field_names = [e.name if type(e) is _Field else e for e in fields]
15
+
16
+ # Some dictionary keys are Python keywords and cannot be used as field names, e.g. `from`.
17
+ # Get around by appending a '_', e.g. dict['from'] => namedtuple.from_
18
+ keymap = [(k.rstrip('_'), k) for k in filter(lambda e: e in ['from_'], field_names)]
19
+
20
+ # extract (non-simple) fields that need conversions
21
+ conversions = [(e.name, e.constructor) for e in fields if type(e) is _Field and e.constructor is not None]
22
+
23
+ # extract default values
24
+ defaults = [e.default if type(e) is _Field else None for e in fields]
25
+
26
+ # Create the base tuple class, with defaults.
27
+ base = collections.namedtuple(typename, field_names)
28
+ base.__new__.__defaults__ = tuple(defaults)
29
+
30
+ class sub(base):
31
+ def __new__(cls, **kwargs):
32
+ # Map keys.
33
+ for oldkey, newkey in keymap:
34
+ if oldkey in kwargs:
35
+ kwargs[newkey] = kwargs[oldkey]
36
+ del kwargs[oldkey]
37
+
38
+ # Any unexpected arguments?
39
+ unexpected = set(kwargs.keys()) - set(super(sub, cls)._fields)
40
+
41
+ # Remove unexpected arguments and issue warning.
42
+ if unexpected:
43
+ for k in unexpected:
44
+ del kwargs[k]
45
+
46
+ s = ('Unexpected fields: ' + ', '.join(unexpected) + ''
47
+ '\nBot API seems to have added new fields to the returned data.'
48
+ ' This version of namedtuple is not able to capture them.'
49
+ '\n\nPlease upgrade telepot by:'
50
+ '\n sudo pip install telepot --upgrade'
51
+ '\n\nIf you still see this message after upgrade, that means I am still working to bring the code up-to-date.'
52
+ ' Please try upgrade again a few days later.'
53
+ ' In the meantime, you can access the new fields the old-fashioned way, through the raw dictionary.')
54
+
55
+ warnings.warn(s, UserWarning)
56
+
57
+ # Convert non-simple values to namedtuples.
58
+ for key, func in conversions:
59
+ if key in kwargs:
60
+ if type(kwargs[key]) is dict:
61
+ kwargs[key] = func(**kwargs[key])
62
+ elif type(kwargs[key]) is list:
63
+ kwargs[key] = func(kwargs[key])
64
+ else:
65
+ raise RuntimeError('Can only convert dict or list')
66
+
67
+ return super(sub, cls).__new__(cls, **kwargs)
68
+
69
+ # https://bugs.python.org/issue24931
70
+ # Python 3.4 bug: namedtuple subclass does not inherit __dict__ properly.
71
+ # Fix it manually.
72
+ if sys.version_info >= (3,4):
73
+ def _asdict(self):
74
+ return collections.OrderedDict(zip(self._fields, self))
75
+ sub._asdict = _asdict
76
+
77
+ sub.__name__ = typename
78
+
79
+ return sub
80
+
81
+ """
82
+ Different treatments for incoming and outgoing namedtuples:
83
+
84
+ - Incoming ones require type declarations for certain fields for deeper parsing.
85
+ - Outgoing ones need no such declarations because users are expected to put the correct object in place.
86
+ """
87
+
88
+ # Namedtuple class will reference other namedtuple classes. Due to circular
89
+ # dependencies, it is impossible to have all class definitions ready at
90
+ # compile time. We have to dynamically obtain class reference at runtime.
91
+ # For example, the following function acts like a constructor for `Message`
92
+ # so any class can reference the Message namedtuple even before the Message
93
+ # namedtuple is defined.
94
+ def _Message(**kwargs):
95
+ return getattr(sys.modules[__name__], 'Message')(**kwargs)
96
+
97
+ # incoming
98
+ User = _create_class('User', [
99
+ 'id',
100
+ 'is_bot',
101
+ 'first_name',
102
+ 'last_name',
103
+ 'username',
104
+ 'language_code'
105
+ ])
106
+
107
+ def UserArray(data):
108
+ return [User(**p) for p in data]
109
+
110
+ # incoming
111
+ ChatPhoto = _create_class('ChatPhoto', [
112
+ 'small_file_id',
113
+ 'big_file_id',
114
+ ])
115
+
116
+ # incoming
117
+ Chat = _create_class('Chat', [
118
+ 'id',
119
+ 'type',
120
+ 'title',
121
+ 'username',
122
+ 'first_name',
123
+ 'last_name',
124
+ 'all_members_are_administrators',
125
+ _Field('photo', constructor=ChatPhoto),
126
+ 'description',
127
+ 'invite_link',
128
+ _Field('pinned_message', constructor=_Message),
129
+ 'sticker_set_name',
130
+ 'can_set_sticker_set',
131
+ ])
132
+
133
+ # incoming
134
+ PhotoSize = _create_class('PhotoSize', [
135
+ 'file_id',
136
+ 'width',
137
+ 'height',
138
+ 'file_size',
139
+ 'file_path', # undocumented
140
+ ])
141
+
142
+ # incoming
143
+ Audio = _create_class('Audio', [
144
+ 'file_id',
145
+ 'duration',
146
+ 'performer',
147
+ 'title',
148
+ 'mime_type',
149
+ 'file_size'
150
+ ])
151
+
152
+ # incoming
153
+ Document = _create_class('Document', [
154
+ 'file_id',
155
+ _Field('thumb', constructor=PhotoSize),
156
+ 'file_name',
157
+ 'mime_type',
158
+ 'file_size',
159
+ 'file_path', # undocumented
160
+ ])
161
+
162
+ # incoming and outgoing
163
+ MaskPosition = _create_class('MaskPosition', [
164
+ 'point',
165
+ 'x_shift',
166
+ 'y_shift',
167
+ 'scale',
168
+ ])
169
+
170
+ # incoming
171
+ Sticker = _create_class('Sticker', [
172
+ 'file_id',
173
+ 'width',
174
+ 'height',
175
+ _Field('thumb', constructor=PhotoSize),
176
+ 'emoji',
177
+ 'set_name',
178
+ _Field('mask_position', constructor=MaskPosition),
179
+ 'file_size',
180
+ ])
181
+
182
+ def StickerArray(data):
183
+ return [Sticker(**p) for p in data]
184
+
185
+ # incoming
186
+ StickerSet = _create_class('StickerSet', [
187
+ 'name',
188
+ 'title',
189
+ 'contains_masks',
190
+ _Field('stickers', constructor=StickerArray),
191
+ ])
192
+
193
+ # incoming
194
+ Video = _create_class('Video', [
195
+ 'file_id',
196
+ 'width',
197
+ 'height',
198
+ 'duration',
199
+ _Field('thumb', constructor=PhotoSize),
200
+ 'mime_type',
201
+ 'file_size',
202
+ 'file_path', # undocumented
203
+ ])
204
+
205
+ # incoming
206
+ Voice = _create_class('Voice', [
207
+ 'file_id',
208
+ 'duration',
209
+ 'mime_type',
210
+ 'file_size'
211
+ ])
212
+
213
+ # incoming
214
+ VideoNote = _create_class('VideoNote', [
215
+ 'file_id',
216
+ 'length',
217
+ 'duration',
218
+ _Field('thumb', constructor=PhotoSize),
219
+ 'file_size'
220
+ ])
221
+
222
+ # incoming
223
+ Contact = _create_class('Contact', [
224
+ 'phone_number',
225
+ 'first_name',
226
+ 'last_name',
227
+ 'user_id'
228
+ ])
229
+
230
+ # incoming
231
+ Location = _create_class('Location', [
232
+ 'longitude',
233
+ 'latitude'
234
+ ])
235
+
236
+ # incoming
237
+ Venue = _create_class('Venue', [
238
+ _Field('location', constructor=Location),
239
+ 'title',
240
+ 'address',
241
+ 'foursquare_id',
242
+ ])
243
+
244
+ # incoming
245
+ File = _create_class('File', [
246
+ 'file_id',
247
+ 'file_size',
248
+ 'file_path'
249
+ ])
250
+
251
+ def PhotoSizeArray(data):
252
+ return [PhotoSize(**p) for p in data]
253
+
254
+ def PhotoSizeArrayArray(data):
255
+ return [[PhotoSize(**p) for p in array] for array in data]
256
+
257
+ # incoming
258
+ UserProfilePhotos = _create_class('UserProfilePhotos', [
259
+ 'total_count',
260
+ _Field('photos', constructor=PhotoSizeArrayArray)
261
+ ])
262
+
263
+ # incoming
264
+ ChatMember = _create_class('ChatMember', [
265
+ _Field('user', constructor=User),
266
+ 'status',
267
+ 'until_date',
268
+ 'can_be_edited',
269
+ 'can_change_info',
270
+ 'can_post_messages',
271
+ 'can_edit_messages',
272
+ 'can_delete_messages',
273
+ 'can_invite_users',
274
+ 'can_restrict_members',
275
+ 'can_pin_messages',
276
+ 'can_promote_members',
277
+ 'can_send_messages',
278
+ 'can_send_media_messages',
279
+ 'can_send_other_messages',
280
+ 'can_add_web_page_previews',
281
+ ])
282
+
283
+ def ChatMemberArray(data):
284
+ return [ChatMember(**p) for p in data]
285
+
286
+ # outgoing
287
+ ReplyKeyboardMarkup = _create_class('ReplyKeyboardMarkup', [
288
+ 'keyboard',
289
+ 'resize_keyboard',
290
+ 'one_time_keyboard',
291
+ 'selective',
292
+ ])
293
+
294
+ # outgoing
295
+ KeyboardButton = _create_class('KeyboardButton', [
296
+ 'text',
297
+ 'request_contact',
298
+ 'request_location',
299
+ ])
300
+
301
+ # outgoing
302
+ ReplyKeyboardRemove = _create_class('ReplyKeyboardRemove', [
303
+ _Field('remove_keyboard', default=True),
304
+ 'selective',
305
+ ])
306
+
307
+ # outgoing
308
+ ForceReply = _create_class('ForceReply', [
309
+ _Field('force_reply', default=True),
310
+ 'selective',
311
+ ])
312
+
313
+ # outgoing
314
+ InlineKeyboardButton = _create_class('InlineKeyboardButton', [
315
+ 'text',
316
+ 'url',
317
+ 'callback_data',
318
+ 'switch_inline_query',
319
+ 'switch_inline_query_current_chat',
320
+ 'callback_game',
321
+ 'pay',
322
+ ])
323
+
324
+ # outgoing
325
+ InlineKeyboardMarkup = _create_class('InlineKeyboardMarkup', [
326
+ 'inline_keyboard',
327
+ ])
328
+
329
+ # incoming
330
+ MessageEntity = _create_class('MessageEntity', [
331
+ 'type',
332
+ 'offset',
333
+ 'length',
334
+ 'url',
335
+ _Field('user', constructor=User),
336
+ ])
337
+
338
+ # incoming
339
+ def MessageEntityArray(data):
340
+ return [MessageEntity(**p) for p in data]
341
+
342
+ # incoming
343
+ GameHighScore = _create_class('GameHighScore', [
344
+ 'position',
345
+ _Field('user', constructor=User),
346
+ 'score',
347
+ ])
348
+
349
+ # incoming
350
+ Animation = _create_class('Animation', [
351
+ 'file_id',
352
+ _Field('thumb', constructor=PhotoSize),
353
+ 'file_name',
354
+ 'mime_type',
355
+ 'file_size',
356
+ ])
357
+
358
+ # incoming
359
+ Game = _create_class('Game', [
360
+ 'title',
361
+ 'description',
362
+ _Field('photo', constructor=PhotoSizeArray),
363
+ 'text',
364
+ _Field('text_entities', constructor=MessageEntityArray),
365
+ _Field('animation', constructor=Animation),
366
+ ])
367
+
368
+ # incoming
369
+ Invoice = _create_class('Invoice', [
370
+ 'title',
371
+ 'description',
372
+ 'start_parameter',
373
+ 'currency',
374
+ 'total_amount',
375
+ ])
376
+
377
+ # outgoing
378
+ LabeledPrice = _create_class('LabeledPrice', [
379
+ 'label',
380
+ 'amount',
381
+ ])
382
+
383
+ # outgoing
384
+ ShippingOption = _create_class('ShippingOption', [
385
+ 'id',
386
+ 'title',
387
+ 'prices',
388
+ ])
389
+
390
+ # incoming
391
+ ShippingAddress = _create_class('ShippingAddress', [
392
+ 'country_code',
393
+ 'state',
394
+ 'city',
395
+ 'street_line1',
396
+ 'street_line2',
397
+ 'post_code',
398
+ ])
399
+
400
+ # incoming
401
+ OrderInfo = _create_class('OrderInfo', [
402
+ 'name',
403
+ 'phone_number',
404
+ 'email',
405
+ _Field('shipping_address', constructor=ShippingAddress),
406
+ ])
407
+
408
+ # incoming
409
+ ShippingQuery = _create_class('ShippingQuery', [
410
+ 'id',
411
+ _Field('from_', constructor=User),
412
+ 'invoice_payload',
413
+ _Field('shipping_address', constructor=ShippingAddress),
414
+ ])
415
+
416
+ # incoming
417
+ PreCheckoutQuery = _create_class('PreCheckoutQuery', [
418
+ 'id',
419
+ _Field('from_', constructor=User),
420
+ 'currency',
421
+ 'total_amount',
422
+ 'invoice_payload',
423
+ 'shipping_option_id',
424
+ _Field('order_info', constructor=OrderInfo),
425
+ ])
426
+
427
+ # incoming
428
+ SuccessfulPayment = _create_class('SuccessfulPayment', [
429
+ 'currency',
430
+ 'total_amount',
431
+ 'invoice_payload',
432
+ 'shipping_option_id',
433
+ _Field('order_info', constructor=OrderInfo),
434
+ 'telegram_payment_charge_id',
435
+ 'provider_payment_charge_id',
436
+ ])
437
+
438
+ # incoming
439
+ Message = _create_class('Message', [
440
+ 'message_id',
441
+ _Field('from_', constructor=User),
442
+ 'date',
443
+ _Field('chat', constructor=Chat),
444
+ _Field('forward_from', constructor=User),
445
+ _Field('forward_from_chat', constructor=Chat),
446
+ 'forward_from_message_id',
447
+ 'forward_signature',
448
+ 'forward_date',
449
+ _Field('reply_to_message', constructor=_Message),
450
+ 'edit_date',
451
+ 'author_signature',
452
+ 'text',
453
+ _Field('entities', constructor=MessageEntityArray),
454
+ _Field('caption_entities', constructor=MessageEntityArray),
455
+ _Field('audio', constructor=Audio),
456
+ _Field('document', constructor=Document),
457
+ _Field('game', constructor=Game),
458
+ _Field('photo', constructor=PhotoSizeArray),
459
+ _Field('sticker', constructor=Sticker),
460
+ _Field('video', constructor=Video),
461
+ _Field('voice', constructor=Voice),
462
+ _Field('video_note', constructor=VideoNote),
463
+ _Field('new_chat_members', constructor=UserArray),
464
+ 'caption',
465
+ _Field('contact', constructor=Contact),
466
+ _Field('location', constructor=Location),
467
+ _Field('venue', constructor=Venue),
468
+ _Field('new_chat_member', constructor=User),
469
+ _Field('left_chat_member', constructor=User),
470
+ 'new_chat_title',
471
+ _Field('new_chat_photo', constructor=PhotoSizeArray),
472
+ 'delete_chat_photo',
473
+ 'group_chat_created',
474
+ 'supergroup_chat_created',
475
+ 'channel_chat_created',
476
+ 'migrate_to_chat_id',
477
+ 'migrate_from_chat_id',
478
+ _Field('pinned_message', constructor=_Message),
479
+ _Field('invoice', constructor=Invoice),
480
+ _Field('successful_payment', constructor=SuccessfulPayment),
481
+ 'connected_website',
482
+ ])
483
+
484
+ # incoming
485
+ InlineQuery = _create_class('InlineQuery', [
486
+ 'id',
487
+ _Field('from_', constructor=User),
488
+ _Field('location', constructor=Location),
489
+ 'query',
490
+ 'offset',
491
+ ])
492
+
493
+ # incoming
494
+ ChosenInlineResult = _create_class('ChosenInlineResult', [
495
+ 'result_id',
496
+ _Field('from_', constructor=User),
497
+ _Field('location', constructor=Location),
498
+ 'inline_message_id',
499
+ 'query',
500
+ ])
501
+
502
+ # incoming
503
+ CallbackQuery = _create_class('CallbackQuery', [
504
+ 'id',
505
+ _Field('from_', constructor=User),
506
+ _Field('message', constructor=Message),
507
+ 'inline_message_id',
508
+ 'chat_instance',
509
+ 'data',
510
+ 'game_short_name',
511
+ ])
512
+
513
+ # incoming
514
+ Update = _create_class('Update', [
515
+ 'update_id',
516
+ _Field('message', constructor=Message),
517
+ _Field('edited_message', constructor=Message),
518
+ _Field('channel_post', constructor=Message),
519
+ _Field('edited_channel_post', constructor=Message),
520
+ _Field('inline_query', constructor=InlineQuery),
521
+ _Field('chosen_inline_result', constructor=ChosenInlineResult),
522
+ _Field('callback_query', constructor=CallbackQuery),
523
+ ])
524
+
525
+ # incoming
526
+ def UpdateArray(data):
527
+ return [Update(**u) for u in data]
528
+
529
+ # incoming
530
+ WebhookInfo = _create_class('WebhookInfo', [
531
+ 'url',
532
+ 'has_custom_certificate',
533
+ 'pending_update_count',
534
+ 'last_error_date',
535
+ 'last_error_message',
536
+ ])
537
+
538
+ # outgoing
539
+ InputTextMessageContent = _create_class('InputTextMessageContent', [
540
+ 'message_text',
541
+ 'parse_mode',
542
+ 'disable_web_page_preview',
543
+ ])
544
+
545
+ # outgoing
546
+ InputLocationMessageContent = _create_class('InputLocationMessageContent', [
547
+ 'latitude',
548
+ 'longitude',
549
+ 'live_period',
550
+ ])
551
+
552
+ # outgoing
553
+ InputVenueMessageContent = _create_class('InputVenueMessageContent', [
554
+ 'latitude',
555
+ 'longitude',
556
+ 'title',
557
+ 'address',
558
+ 'foursquare_id',
559
+ ])
560
+
561
+ # outgoing
562
+ InputContactMessageContent = _create_class('InputContactMessageContent', [
563
+ 'phone_number',
564
+ 'first_name',
565
+ 'last_name',
566
+ ])
567
+
568
+ # outgoing
569
+ InlineQueryResultArticle = _create_class('InlineQueryResultArticle', [
570
+ _Field('type', default='article'),
571
+ 'id',
572
+ 'title',
573
+ 'input_message_content',
574
+ 'reply_markup',
575
+ 'url',
576
+ 'hide_url',
577
+ 'description',
578
+ 'thumb_url',
579
+ 'thumb_width',
580
+ 'thumb_height',
581
+ ])
582
+
583
+ # outgoing
584
+ InlineQueryResultPhoto = _create_class('InlineQueryResultPhoto', [
585
+ _Field('type', default='photo'),
586
+ 'id',
587
+ 'photo_url',
588
+ 'thumb_url',
589
+ 'photo_width',
590
+ 'photo_height',
591
+ 'title',
592
+ 'description',
593
+ 'caption',
594
+ 'parse_mode',
595
+ 'reply_markup',
596
+ 'input_message_content',
597
+ ])
598
+
599
+ # outgoing
600
+ InlineQueryResultGif = _create_class('InlineQueryResultGif', [
601
+ _Field('type', default='gif'),
602
+ 'id',
603
+ 'gif_url',
604
+ 'gif_width',
605
+ 'gif_height',
606
+ 'gif_duration',
607
+ 'thumb_url',
608
+ 'title',
609
+ 'caption',
610
+ 'parse_mode',
611
+ 'reply_markup',
612
+ 'input_message_content',
613
+ ])
614
+
615
+ # outgoing
616
+ InlineQueryResultMpeg4Gif = _create_class('InlineQueryResultMpeg4Gif', [
617
+ _Field('type', default='mpeg4_gif'),
618
+ 'id',
619
+ 'mpeg4_url',
620
+ 'mpeg4_width',
621
+ 'mpeg4_height',
622
+ 'mpeg4_duration',
623
+ 'thumb_url',
624
+ 'title',
625
+ 'caption',
626
+ 'parse_mode',
627
+ 'reply_markup',
628
+ 'input_message_content',
629
+ ])
630
+
631
+ # outgoing
632
+ InlineQueryResultVideo = _create_class('InlineQueryResultVideo', [
633
+ _Field('type', default='video'),
634
+ 'id',
635
+ 'video_url',
636
+ 'mime_type',
637
+ 'thumb_url',
638
+ 'title',
639
+ 'caption',
640
+ 'parse_mode',
641
+ 'video_width',
642
+ 'video_height',
643
+ 'video_duration',
644
+ 'description',
645
+ 'reply_markup',
646
+ 'input_message_content',
647
+ ])
648
+
649
+ # outgoing
650
+ InlineQueryResultAudio = _create_class('InlineQueryResultAudio', [
651
+ _Field('type', default='audio'),
652
+ 'id',
653
+ 'audio_url',
654
+ 'title',
655
+ 'caption',
656
+ 'parse_mode',
657
+ 'performer',
658
+ 'audio_duration',
659
+ 'reply_markup',
660
+ 'input_message_content',
661
+ ])
662
+
663
+ # outgoing
664
+ InlineQueryResultVoice = _create_class('InlineQueryResultVoice', [
665
+ _Field('type', default='voice'),
666
+ 'id',
667
+ 'voice_url',
668
+ 'title',
669
+ 'caption',
670
+ 'parse_mode',
671
+ 'voice_duration',
672
+ 'reply_markup',
673
+ 'input_message_content',
674
+ ])
675
+
676
+ # outgoing
677
+ InlineQueryResultDocument = _create_class('InlineQueryResultDocument', [
678
+ _Field('type', default='document'),
679
+ 'id',
680
+ 'title',
681
+ 'caption',
682
+ 'parse_mode',
683
+ 'document_url',
684
+ 'mime_type',
685
+ 'description',
686
+ 'reply_markup',
687
+ 'input_message_content',
688
+ 'thumb_url',
689
+ 'thumb_width',
690
+ 'thumb_height',
691
+ ])
692
+
693
+ # outgoing
694
+ InlineQueryResultLocation = _create_class('InlineQueryResultLocation', [
695
+ _Field('type', default='location'),
696
+ 'id',
697
+ 'latitude',
698
+ 'longitude',
699
+ 'title',
700
+ 'live_period',
701
+ 'reply_markup',
702
+ 'input_message_content',
703
+ 'thumb_url',
704
+ 'thumb_width',
705
+ 'thumb_height',
706
+ ])
707
+
708
+ # outgoing
709
+ InlineQueryResultVenue = _create_class('InlineQueryResultVenue', [
710
+ _Field('type', default='venue'),
711
+ 'id',
712
+ 'latitude',
713
+ 'longitude',
714
+ 'title',
715
+ 'address',
716
+ 'foursquare_id',
717
+ 'reply_markup',
718
+ 'input_message_content',
719
+ 'thumb_url',
720
+ 'thumb_width',
721
+ 'thumb_height',
722
+ ])
723
+
724
+ # outgoing
725
+ InlineQueryResultContact = _create_class('InlineQueryResultContact', [
726
+ _Field('type', default='contact'),
727
+ 'id',
728
+ 'phone_number',
729
+ 'first_name',
730
+ 'last_name',
731
+ 'reply_markup',
732
+ 'input_message_content',
733
+ 'thumb_url',
734
+ 'thumb_width',
735
+ 'thumb_height',
736
+ ])
737
+
738
+ # outgoing
739
+ InlineQueryResultGame = _create_class('InlineQueryResultGame', [
740
+ _Field('type', default='game'),
741
+ 'id',
742
+ 'game_short_name',
743
+ 'reply_markup',
744
+ ])
745
+
746
+ # outgoing
747
+ InlineQueryResultCachedPhoto = _create_class('InlineQueryResultCachedPhoto', [
748
+ _Field('type', default='photo'),
749
+ 'id',
750
+ 'photo_file_id',
751
+ 'title',
752
+ 'description',
753
+ 'caption',
754
+ 'parse_mode',
755
+ 'reply_markup',
756
+ 'input_message_content',
757
+ ])
758
+
759
+ # outgoing
760
+ InlineQueryResultCachedGif = _create_class('InlineQueryResultCachedGif', [
761
+ _Field('type', default='gif'),
762
+ 'id',
763
+ 'gif_file_id',
764
+ 'title',
765
+ 'caption',
766
+ 'parse_mode',
767
+ 'reply_markup',
768
+ 'input_message_content',
769
+ ])
770
+
771
+ # outgoing
772
+ InlineQueryResultCachedMpeg4Gif = _create_class('InlineQueryResultCachedMpeg4Gif', [
773
+ _Field('type', default='mpeg4_gif'),
774
+ 'id',
775
+ 'mpeg4_file_id',
776
+ 'title',
777
+ 'caption',
778
+ 'parse_mode',
779
+ 'reply_markup',
780
+ 'input_message_content',
781
+ ])
782
+
783
+ # outgoing
784
+ InlineQueryResultCachedSticker = _create_class('InlineQueryResultCachedSticker', [
785
+ _Field('type', default='sticker'),
786
+ 'id',
787
+ 'sticker_file_id',
788
+ 'reply_markup',
789
+ 'input_message_content',
790
+ ])
791
+
792
+ # outgoing
793
+ InlineQueryResultCachedDocument = _create_class('InlineQueryResultCachedDocument', [
794
+ _Field('type', default='document'),
795
+ 'id',
796
+ 'title',
797
+ 'document_file_id',
798
+ 'description',
799
+ 'caption',
800
+ 'parse_mode',
801
+ 'reply_markup',
802
+ 'input_message_content',
803
+ ])
804
+
805
+ # outgoing
806
+ InlineQueryResultCachedVideo = _create_class('InlineQueryResultCachedVideo', [
807
+ _Field('type', default='video'),
808
+ 'id',
809
+ 'video_file_id',
810
+ 'title',
811
+ 'description',
812
+ 'caption',
813
+ 'parse_mode',
814
+ 'reply_markup',
815
+ 'input_message_content',
816
+ ])
817
+
818
+ # outgoing
819
+ InlineQueryResultCachedVoice = _create_class('InlineQueryResultCachedVoice', [
820
+ _Field('type', default='voice'),
821
+ 'id',
822
+ 'voice_file_id',
823
+ 'title',
824
+ 'caption',
825
+ 'parse_mode',
826
+ 'reply_markup',
827
+ 'input_message_content',
828
+ ])
829
+
830
+ # outgoing
831
+ InlineQueryResultCachedAudio = _create_class('InlineQueryResultCachedAudio', [
832
+ _Field('type', default='audio'),
833
+ 'id',
834
+ 'audio_file_id',
835
+ 'caption',
836
+ 'parse_mode',
837
+ 'reply_markup',
838
+ 'input_message_content',
839
+ ])
840
+
841
+ # outgoing
842
+ InputMediaPhoto = _create_class('InputMediaPhoto', [
843
+ _Field('type', default='photo'),
844
+ 'media',
845
+ 'caption',
846
+ 'parse_mode',
847
+ ])
848
+
849
+ # outgoing
850
+ InputMediaVideo = _create_class('InputMediaVideo', [
851
+ _Field('type', default='video'),
852
+ 'media',
853
+ 'caption',
854
+ 'parse_mode',
855
+ 'width',
856
+ 'height',
857
+ 'duration',
858
+ 'supports_streaming',
859
+ ])
860
+
861
+ # incoming
862
+ ResponseParameters = _create_class('ResponseParameters', [
863
+ 'migrate_to_chat_id',
864
+ 'retry_after',
865
+ ])
telepot/routing.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This module has a bunch of key function factories and routing table factories
3
+ to facilitate the use of :class:`.Router`.
4
+
5
+ Things to remember:
6
+
7
+ 1. A key function takes one argument - the message, and returns a key, optionally
8
+ followed by positional arguments and keyword arguments.
9
+
10
+ 2. A routing table is just a dictionary. After obtaining one from a factory
11
+ function, you can customize it to your liking.
12
+ """
13
+
14
+ import re
15
+ from . import glance, _isstring, all_content_types
16
+
17
+ def by_content_type():
18
+ """
19
+ :return:
20
+ A key function that returns a 2-tuple (content_type, (msg[content_type],)).
21
+ In plain English, it returns the message's *content type* as the key,
22
+ and the corresponding content as a positional argument to the handler
23
+ function.
24
+ """
25
+ def f(msg):
26
+ content_type = glance(msg, flavor='chat')[0]
27
+ return content_type, (msg[content_type],)
28
+ return f
29
+
30
+ def by_command(extractor, prefix=('/',), separator=' ', pass_args=False):
31
+ """
32
+ :param extractor:
33
+ a function that takes one argument (the message) and returns a portion
34
+ of message to be interpreted. To extract the text of a chat message,
35
+ use ``lambda msg: msg['text']``.
36
+
37
+ :param prefix:
38
+ a list of special characters expected to indicate the head of a command.
39
+
40
+ :param separator:
41
+ a command may be followed by arguments separated by ``separator``.
42
+
43
+ :type pass_args: bool
44
+ :param pass_args:
45
+ If ``True``, arguments following a command will be passed to the handler
46
+ function.
47
+
48
+ :return:
49
+ a key function that interprets a specific part of a message and returns
50
+ the embedded command, optionally followed by arguments. If the text is
51
+ not preceded by any of the specified ``prefix``, it returns a 1-tuple
52
+ ``(None,)`` as the key. This is to distinguish with the special
53
+ ``None`` key in routing table.
54
+ """
55
+ if not isinstance(prefix, (tuple, list)):
56
+ prefix = (prefix,)
57
+
58
+ def f(msg):
59
+ text = extractor(msg)
60
+ for px in prefix:
61
+ if text.startswith(px):
62
+ chunks = text[len(px):].split(separator)
63
+ return chunks[0], (chunks[1:],) if pass_args else ()
64
+ return (None,), # to distinguish with `None`
65
+ return f
66
+
67
+ def by_chat_command(prefix=('/',), separator=' ', pass_args=False):
68
+ """
69
+ :param prefix:
70
+ a list of special characters expected to indicate the head of a command.
71
+
72
+ :param separator:
73
+ a command may be followed by arguments separated by ``separator``.
74
+
75
+ :type pass_args: bool
76
+ :param pass_args:
77
+ If ``True``, arguments following a command will be passed to the handler
78
+ function.
79
+
80
+ :return:
81
+ a key function that interprets a chat message's text and returns
82
+ the embedded command, optionally followed by arguments. If the text is
83
+ not preceded by any of the specified ``prefix``, it returns a 1-tuple
84
+ ``(None,)`` as the key. This is to distinguish with the special
85
+ ``None`` key in routing table.
86
+ """
87
+ return by_command(lambda msg: msg['text'], prefix, separator, pass_args)
88
+
89
+ def by_text():
90
+ """
91
+ :return:
92
+ a key function that returns a message's ``text`` field.
93
+ """
94
+ return lambda msg: msg['text']
95
+
96
+ def by_data():
97
+ """
98
+ :return:
99
+ a key function that returns a message's ``data`` field.
100
+ """
101
+ return lambda msg: msg['data']
102
+
103
+ def by_regex(extractor, regex, key=1):
104
+ """
105
+ :param extractor:
106
+ a function that takes one argument (the message) and returns a portion
107
+ of message to be interpreted. To extract the text of a chat message,
108
+ use ``lambda msg: msg['text']``.
109
+
110
+ :type regex: str or regex object
111
+ :param regex: the pattern to look for
112
+
113
+ :param key: the part of match object to be used as key
114
+
115
+ :return:
116
+ a key function that returns ``match.group(key)`` as key (where ``match``
117
+ is the match object) and the match object as a positional argument.
118
+ If no match is found, it returns a 1-tuple ``(None,)`` as the key.
119
+ This is to distinguish with the special ``None`` key in routing table.
120
+ """
121
+ if _isstring(regex):
122
+ regex = re.compile(regex)
123
+
124
+ def f(msg):
125
+ text = extractor(msg)
126
+ match = regex.search(text)
127
+ if match:
128
+ index = key if isinstance(key, tuple) else (key,)
129
+ return match.group(*index), (match,)
130
+ else:
131
+ return (None,), # to distinguish with `None`
132
+ return f
133
+
134
+ def process_key(processor, fn):
135
+ """
136
+ :param processor:
137
+ a function to process the key returned by the supplied key function
138
+
139
+ :param fn:
140
+ a key function
141
+
142
+ :return:
143
+ a function that wraps around the supplied key function to further
144
+ process the key before returning.
145
+ """
146
+ def f(*aa, **kw):
147
+ k = fn(*aa, **kw)
148
+ if isinstance(k, (tuple, list)):
149
+ return (processor(k[0]),) + tuple(k[1:])
150
+ else:
151
+ return processor(k)
152
+ return f
153
+
154
+ def lower_key(fn):
155
+ """
156
+ :param fn: a key function
157
+
158
+ :return:
159
+ a function that wraps around the supplied key function to ensure
160
+ the returned key is in lowercase.
161
+ """
162
+ def lower(key):
163
+ try:
164
+ return key.lower()
165
+ except AttributeError:
166
+ return key
167
+ return process_key(lower, fn)
168
+
169
+ def upper_key(fn):
170
+ """
171
+ :param fn: a key function
172
+
173
+ :return:
174
+ a function that wraps around the supplied key function to ensure
175
+ the returned key is in uppercase.
176
+ """
177
+ def upper(key):
178
+ try:
179
+ return key.upper()
180
+ except AttributeError:
181
+ return key
182
+ return process_key(upper, fn)
183
+
184
+ def make_routing_table(obj, keys, prefix='on_'):
185
+ """
186
+ :return:
187
+ a dictionary roughly equivalent to ``{'key1': obj.on_key1, 'key2': obj.on_key2, ...}``,
188
+ but ``obj`` does not have to define all methods. It may define the needed ones only.
189
+
190
+ :param obj: the object
191
+
192
+ :param keys: a list of keys
193
+
194
+ :param prefix: a string to be prepended to keys to make method names
195
+ """
196
+ def maptuple(k):
197
+ if isinstance(k, tuple):
198
+ if len(k) == 2:
199
+ return k
200
+ elif len(k) == 1:
201
+ return k[0], lambda *aa, **kw: getattr(obj, prefix+k[0])(*aa, **kw)
202
+ else:
203
+ raise ValueError()
204
+ else:
205
+ return k, lambda *aa, **kw: getattr(obj, prefix+k)(*aa, **kw)
206
+ # Use `lambda` to delay evaluation of `getattr`.
207
+ # I don't want to require definition of all methods.
208
+ # Let users define only the ones he needs.
209
+
210
+ return dict([maptuple(k) for k in keys])
211
+
212
+ def make_content_type_routing_table(obj, prefix='on_'):
213
+ """
214
+ :return:
215
+ a dictionary covering all available content types, roughly equivalent to
216
+ ``{'text': obj.on_text, 'photo': obj.on_photo, ...}``,
217
+ but ``obj`` does not have to define all methods. It may define the needed ones only.
218
+
219
+ :param obj: the object
220
+
221
+ :param prefix: a string to be prepended to content types to make method names
222
+ """
223
+ return make_routing_table(obj, all_content_types, prefix)
telepot/text.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def _apply_entities(text, entities, escape_map, format_map):
2
+ def inside_entities(i):
3
+ return any(map(lambda e:
4
+ e['offset'] <= i < e['offset']+e['length'],
5
+ entities))
6
+
7
+ # Split string into char sequence and escape in-place to
8
+ # preserve index positions.
9
+ seq = list(map(lambda c,i:
10
+ escape_map[c] # escape special characters
11
+ if c in escape_map and not inside_entities(i)
12
+ else c,
13
+ list(text), # split string to char sequence
14
+ range(0,len(text)))) # along with each char's index
15
+
16
+ # Ensure smaller offsets come first
17
+ sorted_entities = sorted(entities, key=lambda e: e['offset'])
18
+ offset = 0
19
+ result = ''
20
+
21
+ for e in sorted_entities:
22
+ f,n,t = e['offset'], e['length'], e['type']
23
+
24
+ result += ''.join(seq[offset:f])
25
+
26
+ if t in format_map:
27
+ # apply format
28
+ result += format_map[t](''.join(seq[f:f+n]), e)
29
+ else:
30
+ result += ''.join(seq[f:f+n])
31
+
32
+ offset = f + n
33
+
34
+ result += ''.join(seq[offset:])
35
+ return result
36
+
37
+
38
+ def apply_entities_as_markdown(text, entities):
39
+ """
40
+ Format text as Markdown. Also take care of escaping special characters.
41
+ Returned value can be passed to :meth:`.Bot.sendMessage` with appropriate
42
+ ``parse_mode``.
43
+
44
+ :param text:
45
+ plain text
46
+
47
+ :param entities:
48
+ a list of `MessageEntity <https://core.telegram.org/bots/api#messageentity>`_ objects
49
+ """
50
+ escapes = {'*': '\\*',
51
+ '_': '\\_',
52
+ '[': '\\[',
53
+ '`': '\\`',}
54
+
55
+ formatters = {'bold': lambda s,e: '*'+s+'*',
56
+ 'italic': lambda s,e: '_'+s+'_',
57
+ 'text_link': lambda s,e: '['+s+']('+e['url']+')',
58
+ 'text_mention': lambda s,e: '['+s+'](tg://user?id='+str(e['user']['id'])+')',
59
+ 'code': lambda s,e: '`'+s+'`',
60
+ 'pre': lambda s,e: '```text\n'+s+'```'}
61
+
62
+ return _apply_entities(text, entities, escapes, formatters)
63
+
64
+
65
+ def apply_entities_as_html(text, entities):
66
+ """
67
+ Format text as HTML. Also take care of escaping special characters.
68
+ Returned value can be passed to :meth:`.Bot.sendMessage` with appropriate
69
+ ``parse_mode``.
70
+
71
+ :param text:
72
+ plain text
73
+
74
+ :param entities:
75
+ a list of `MessageEntity <https://core.telegram.org/bots/api#messageentity>`_ objects
76
+ """
77
+ escapes = {'<': '&lt;',
78
+ '>': '&gt;',
79
+ '&': '&amp;',}
80
+
81
+ formatters = {'bold': lambda s,e: '<b>'+s+'</b>',
82
+ 'italic': lambda s,e: '<i>'+s+'</i>',
83
+ 'text_link': lambda s,e: '<a href="'+e['url']+'">'+s+'</a>',
84
+ 'text_mention': lambda s,e: '<a href="tg://user?id='+str(e['user']['id'])+'">'+s+'</a>',
85
+ 'code': lambda s,e: '<code>'+s+'</code>',
86
+ 'pre': lambda s,e: '<pre>'+s+'</pre>'}
87
+
88
+ return _apply_entities(text, entities, escapes, formatters)