Spaces:
Build error
Build error
Commit ·
0408b90
1
Parent(s): bc7e119
Upload 42 files
Browse files- telepot-12.7.dist-info/INSTALLER +1 -0
- telepot-12.7.dist-info/METADATA +24 -0
- telepot-12.7.dist-info/RECORD +42 -0
- telepot-12.7.dist-info/REQUESTED +0 -0
- telepot-12.7.dist-info/WHEEL +5 -0
- telepot-12.7.dist-info/top_level.txt +1 -0
- telepot/__init__.py +1411 -0
- telepot/__pycache__/__init__.cpython-311.pyc +0 -0
- telepot/__pycache__/api.cpython-311.pyc +0 -0
- telepot/__pycache__/delegate.cpython-311.pyc +0 -0
- telepot/__pycache__/exception.cpython-311.pyc +0 -0
- telepot/__pycache__/filtering.cpython-311.pyc +0 -0
- telepot/__pycache__/hack.cpython-311.pyc +0 -0
- telepot/__pycache__/helper.cpython-311.pyc +0 -0
- telepot/__pycache__/loop.cpython-311.pyc +0 -0
- telepot/__pycache__/namedtuple.cpython-311.pyc +0 -0
- telepot/__pycache__/routing.cpython-311.pyc +0 -0
- telepot/__pycache__/text.cpython-311.pyc +0 -0
- telepot/aio/__init__.py +926 -0
- telepot/aio/__pycache__/__init__.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/api.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/delegate.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/hack.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/helper.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/loop.cpython-311.pyc +0 -0
- telepot/aio/__pycache__/routing.cpython-311.pyc +0 -0
- telepot/aio/api.py +168 -0
- telepot/aio/delegate.py +106 -0
- telepot/aio/hack.py +36 -0
- telepot/aio/helper.py +372 -0
- telepot/aio/loop.py +205 -0
- telepot/aio/routing.py +46 -0
- telepot/api.py +164 -0
- telepot/delegate.py +420 -0
- telepot/exception.py +111 -0
- telepot/filtering.py +34 -0
- telepot/hack.py +16 -0
- telepot/helper.py +1170 -0
- telepot/loop.py +313 -0
- telepot/namedtuple.py +865 -0
- telepot/routing.py +223 -0
- 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 = {'<': '<',
|
| 78 |
+
'>': '>',
|
| 79 |
+
'&': '&',}
|
| 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)
|