1
0
mirror of https://github.com/eternnoir/pyTelegramBotAPI.git synced 2023-08-10 21:12:57 +03:00

Compare commits

...

130 Commits

Author SHA1 Message Date
Badiboy
b9b4885568 Merge pull request #1597 from Badiboy/master
Copyright update
2022-06-29 19:25:40 +03:00
Badiboy
ce0a974c91 Copyright update 2022-06-29 19:24:27 +03:00
Badiboy
6ff015689a Merge pull request #1595 from coder2020official/master
Add the possibility to getbase class of a State object
2022-06-29 18:34:06 +03:00
_run
6459aded82 Merge branch 'eternnoir:master' into master 2022-06-29 20:22:59 +05:00
_run
fec47cecaf Add the possibility to getbase class of a State object 2022-06-29 20:21:42 +05:00
Badiboy
3892b0fb80 Merge pull request #1593 from coder2020official/master
Fixed previous fix 🤦‍♂️
2022-06-29 12:58:43 +03:00
_run
fbb9a73fc0 F###, forgot to put await 2022-06-29 14:52:37 +05:00
Badiboy
db5c53b8e5 Merge pull request #1591 from coder2020official/master
Pass only the necessary data
2022-06-28 17:58:35 +03:00
_run
6e8abc709e Pass only the necessary data 2022-06-28 19:51:51 +05:00
Badiboy
752f35614c Merge pull request #1587 from coder2020official/master
Middleware update: everything in data will be passed to handler if ne…
2022-06-26 12:09:38 +03:00
_run
a2893945b2 Async changes and sync improvements 2022-06-25 22:15:53 +05:00
_run
1686ce4f44 Middleware update: everything in data will be passed to handler if needed. 2022-06-25 21:48:44 +05:00
Badiboy
24b1129c8a Merge pull request #1584 from coder2020official/master
Bot API 6.1 update
2022-06-23 10:50:39 +03:00
coder2020official
7f9dac4147 Fix previous commit 2022-06-23 12:46:27 +05:00
Badiboy
f96775e9eb Merge pull request #1585 from Prikalel/master
Rename `midddleware` to `middleware` in examples.
2022-06-23 10:41:20 +03:00
coder2020official
1342cab259 Fix boolean parameters 2022-06-23 12:05:26 +05:00
Alex Poklonsky
dd8125cbd0 Rename midddleware to middleware in examples. 2022-06-23 09:09:37 +03:00
coder2020official
8769802744 Bump version to 4.6.0 2022-06-21 15:44:42 +05:00
coder2020official
a7fb8a2bec Implemented some tests 2022-06-21 15:28:01 +05:00
coder2020official
d7f34ae370 Fix the typo 2022-06-21 15:27:45 +05:00
coder2020official
7d931abe37 Added secret token and create_invoice_link for asynctelebot 2022-06-21 15:22:00 +05:00
coder2020official
f52124827f Added all new changes for types (is_premium, added_to_attachment_menu, and etc) 2022-06-21 15:21:35 +05:00
coder2020official
0d0f9ef330 Added secret token and create_invoice_link 2022-06-21 15:20:55 +05:00
_run
24cdcb1bcc Update README.md 2022-06-21 14:22:57 +05:00
Badiboy
7994a80a00 Merge pull request #1580 from e323w/master
Fixed async long polling
2022-06-15 11:21:12 +03:00
e323w
b2662274f9 Fixed async long polling 2022-06-15 03:24:51 +04:00
Badiboy
a21ab203a1 Merge pull request #1575 from coder2020official/bugfixes
Fix bug with unsaving data
2022-06-04 19:44:47 +03:00
coder2020official
e689e968db Fix bug with unsaving data 2022-06-04 21:33:05 +05:00
Badiboy
808810e6d6 Merge pull request #1574 from Mayson90/master
add hydrolib_bot to Bots using this library
2022-06-01 19:14:59 +03:00
Andrey M
5a60846c7f add hydrolib_bot to Bots using this library 2022-06-01 19:07:54 +03:00
Badiboy
d2c1615392 Merge pull request #1573 from tusharhero/master
Added my bot made using pyTelegramBotAPI
2022-06-01 10:56:01 +03:00
Tushar maharana
7cdb48c3e0 brought my bot ot end 2022-06-01 10:22:22 +05:30
Tushar maharana
28ae0867f8 Added my bot made using pyTelegramBotAPI
source code available here 
https://github.com/tusharhero/bincode-telegram-bot
2022-05-31 22:58:03 +05:30
Badiboy
e84cc0e0af Merge pull request #1571 from TECH-SAVVY-GUY/patch-1
Added `WebApp` functions
2022-05-30 15:25:05 +03:00
Soham Datta
42ce47914d Fix typo in WebApp() functions 2022-05-30 17:21:33 +05:30
Soham Datta
4401780ba9 Update WebApp() functions
Adjusted function name case to pythonic style.
2022-05-30 17:18:03 +05:30
Soham Datta
82f056e88a Added WebApp functions
`validate_WebApp_data()` - Use to validate the data received by the WebApp from Telegram.

`parse_WebApp_data()` - Use to parse the data sent to the WebApp from the bot's backend.
2022-05-30 14:52:49 +05:30
Badiboy
6b19d631d1 Merge pull request #1565 from DevAdvik/master
Update create_invite_link.py
2022-05-23 14:16:42 +03:00
Advik Singh Somvanshi
ee7adb00df Re-fix inlinekeyboardmarkup 2022-05-23 13:27:48 +05:30
Badiboy
82a8aa65f0 Merge pull request #1566 from coder2020official/master
More description for class-based middlewares in readme.
2022-05-22 11:21:49 +03:00
_run
72aaf44dc7 Update README.md 2022-05-22 01:50:38 +05:00
Advik Singh Somvanshi
1d0efce76e Update create_invite_link.py
Fix
2022-05-22 02:17:21 +05:30
Advik Singh Somvanshi
74d0604c05 Update create_invite_link.py
Bug fix (Real FINAL FIX!!)
2022-05-22 01:54:02 +05:30
Badiboy
1943f659bc Merge pull request #1561 from coder2020official/bugfixes
Aiohttp client session management improvements.
2022-05-21 23:22:29 +03:00
Advik Singh Somvanshi
d6ec104829 Update create_invite_link.py
Imported both inline keyboards in same line:|
2022-05-22 01:48:48 +05:30
Advik Singh Somvanshi
7edaa51995 Update create_invite_link.py
Removed while loop
2022-05-22 01:43:24 +05:30
Advik Singh Somvanshi
6bb47e9a44 Update create_invite_link.py
Added .types while importing inline markup keyboards (fix)
Removed threading import since message is not to be deleted
2022-05-22 01:38:32 +05:30
coder2020official
8da749ee05 Remove ssl 2022-05-22 01:02:55 +05:00
coder2020official
0c59d1187e Update requirements.txt 2022-05-22 00:11:16 +05:00
Badiboy
e9d1d98f03 Merge pull request #1562 from coder2020official/newfeatures
Added ability to hide a link in text
2022-05-21 16:51:51 +03:00
Badiboy
c63b0d6a3d Merge pull request #1560 from coder2020official/master
Changed behaviour of default parse mode in sync and async telebot.
2022-05-21 16:51:17 +03:00
coder2020official
388306b7fe Updated examples 2022-05-21 17:45:59 +05:00
coder2020official
5e3fd17436 Added ability to hide a link in text 2022-05-21 17:39:45 +05:00
coder2020official
ccc09ffaf3 Aiohttp client session management improvements. 2022-05-21 17:38:16 +05:00
_run
e086303535 Merge branch 'eternnoir:master' into master 2022-05-21 17:11:14 +05:00
coder2020official
d6e93f85f1 Improved the comment for quick_markup 2022-05-21 17:10:57 +05:00
coder2020official
d954f8f5b3 Fixed default parse mode in asynctelebot 2022-05-21 17:10:45 +05:00
coder2020official
33375ac135 Added ability to set default parse_mode for explanation_parse_mode in send_poll 2022-05-21 17:10:29 +05:00
coder2020official
28662876a2 Add workspace exception in gitignore 2022-05-21 17:09:59 +05:00
Badiboy
0f44fd3c40 Merge pull request #1558 from DevAdvik/master
Created create_invite_link.py
2022-05-20 22:48:10 +03:00
Advik Singh Somvanshi
8fefd7b5b3 Create create_invite_link.py
Added create_chat_invite_link() func example
2022-05-20 23:00:38 +05:30
Badiboy
7567750276 Merge pull request #1552 from m-cyx/patch-1
Small srtring formatter fix
2022-05-15 23:15:12 +03:00
m-cyx
f526a9d8a4 Small srtring formatter fix 2022-05-15 22:46:15 +03:00
Badiboy
feffe726dd Merge pull request #1551 from everpcpc/patch-1
fix: warning none_stop parameter is deprecated
2022-05-15 10:57:59 +03:00
everpcpc
3ff7e28467 fix: warning none_stop parameter is deprecated 2022-05-15 14:29:29 +08:00
Badiboy
5d74e18d1a Merge pull request #1550 from Badiboy/master
Poll type parameter parse fix
2022-05-15 01:06:04 +03:00
Badiboy
91b665ea94 Poll type parameter parse fix
Plus some typo
2022-05-15 00:59:35 +03:00
Badiboy
6e4c63b09c Merge pull request #1549 from coder2020official/master
Set escape=True by default.
2022-05-14 19:27:44 +03:00
coder2020official
42efb8488c Set escape=True by default. 2022-05-14 20:32:28 +05:00
Badiboy
03a567ea93 Merge pull request #1548 from AHOHNMYC/copy-message-fix
CopyMessage return type fix
2022-05-14 16:30:29 +03:00
AHOHNMYC
3f28bb6e9d CopyMessage return type fix [async] 2022-05-14 15:48:27 +03:00
AHOHNMYC
e051dda113 CopyMessage return type fix 2022-05-14 15:46:05 +03:00
Badiboy
856debe7d2 Merge pull request #1542 from abdullaev388/master
Added examples of multibot
2022-05-13 15:11:39 +03:00
abdullaev388
42955d1886 added examples of multibot 2022-05-11 10:50:33 +05:00
Badiboy
a2f3cd03e1 Merge pull request #1540 from Badiboy/master
class File parse fix
2022-05-08 23:39:00 +03:00
Badiboy
7e68721475 class File parse fix 2022-05-08 23:34:56 +03:00
Badiboy
7965ba4f69 Merge pull request #1538 from coder2020official/master
Fix typo in readme
2022-05-08 18:45:32 +03:00
Badiboy
aba4d3e246 Merge pull request #1536 from atif5/master
Updated the "bots using this api" section in README
2022-05-08 18:45:07 +03:00
_run
caae6bb93f Update README.md 2022-05-08 18:13:07 +05:00
Burzum
096d7a4eea Update README.md 2022-05-08 00:02:40 +03:00
Burzum
ab4140ba9f Update README.md 2022-05-08 00:01:45 +03:00
Burzum
77c3587012 Update README.md 2022-05-08 00:00:46 +03:00
Badiboy
efb1b44e59 Merge pull request #1535 from Badiboy/master
Bump version to 4.5.1
2022-05-07 22:41:19 +03:00
Badiboy
2c8793b794 Bump version to 4.5.1 2022-05-07 22:40:26 +03:00
Badiboy
146fd57b10 Merge pull request #1531 from coder2020official/patch-3
Random unused import deleted
2022-05-05 09:26:57 +03:00
_run
8a12ae3565 Update redis_storage.py 2022-05-04 19:55:43 +05:00
Badiboy
2d8c2312e3 Merge pull request #1529 from coder2020official/state-fixes
Allow only state objects
2022-05-03 13:02:24 +03:00
coder2020official
f9cd0d7e08 Avoid circular import 2022-05-02 14:45:43 +05:00
Badiboy
59cd1a00e7 Merge pull request #1528 from coder2020official/master
Seo improvements for documentation.
2022-05-02 00:30:55 +03:00
coder2020official
836130a718 Allow only state objects 2022-05-02 02:08:48 +05:00
_run
a7db2d8d9c Merge branch 'eternnoir:master' into master 2022-05-01 23:20:02 +05:00
coder2020official
c022d49996 SEO improvements for documentation 2022-05-01 23:19:46 +05:00
Badiboy
825827cb1e Merge pull request #1527 from coder2020official/proxy-fix
Fixed proxy for asynctelebot
2022-05-01 17:26:54 +03:00
_run
cd92b70d6b Update README.md 2022-05-01 19:13:49 +05:00
Badiboy
617c990994 Merge pull request #1526 from coder2020official/master
Key for custom filters
2022-05-01 17:01:37 +03:00
coder2020official
9b959373db Fixed proxy for asynctelebot 2022-05-01 18:43:07 +05:00
coder2020official
76c0197ab7 Key for custom filters 2022-05-01 16:11:00 +05:00
Badiboy
7d9658b062 Merge pull request #1525 from Badiboy/master
Polling exception logging updated
2022-05-01 14:00:50 +03:00
Badiboy
db0c946780 Polling exception logging updated
Polling exception logging arranged with infinity_polling mode
2022-05-01 00:17:14 +03:00
Badiboy
c6202da36f Merge pull request #1524 from coder2020official/master
Markdown & Html functions added(Beta version, still in progress)
2022-04-30 23:43:53 +03:00
coder2020official
532011138c Added examples for formatting 2022-05-01 00:58:06 +05:00
coder2020official
191164cba0 Fix traceback 2022-05-01 00:45:34 +05:00
coder2020official
5688aaa03b Markdown & Html functions added(Beta version, still in progress) 2022-05-01 00:28:00 +05:00
Badiboy
88a76c0a15 Merge pull request #1521 from Badiboy/master
Mistake in ChatAdministratorRights
2022-04-24 23:42:51 +03:00
Badiboy
e1dc6d7beb Mistake in ChatAdministratorRights 2022-04-24 23:41:08 +03:00
Badiboy
730d11012d Merge pull request #1517 from Badiboy/master
Bugfix in answer_web_app_query
2022-04-24 11:40:42 +03:00
Badiboy
b43b636ba0 Bugfix in answer_web_app_query 2022-04-24 11:33:19 +03:00
Badiboy
a7ca6c057e Merge pull request #1516 from Badiboy/master
i18n middleware - file revert
2022-04-24 11:29:47 +03:00
Badiboy
bd002c6429 i18n middleware - file revert 2022-04-24 11:28:20 +03:00
Badiboy
453df01f26 Merge pull request #1511 from abdullaev388/master
Added sync i18n class based middleware
2022-04-24 11:27:12 +03:00
abdullaev388
24ae38cca6 added function based middleware i18n example 2022-04-24 12:51:01 +05:00
abdullaev388
3b386965ea sync middleware examples separated into two folders 2022-04-24 11:52:01 +05:00
Badiboy
5077289d0d Merge pull request #1513 from Badiboy/master
typo
2022-04-23 20:20:17 +03:00
Badiboy
a54b21cb50 type 2022-04-23 20:19:25 +03:00
Badiboy
fa80cb0002 Merge pull request #1512 from Badiboy/master
Bot API 6.0. Deprecation fixes
2022-04-23 19:37:01 +03:00
Badiboy
ad5b92b650 Remove incorrect deprecation 2022-04-23 19:35:38 +03:00
Badiboy
9b1b324ab4 Bump version to 4.5.0 2022-04-23 16:33:59 +03:00
Badiboy
e444bc2a0b Payment example fix 2022-04-23 16:27:53 +03:00
Badiboy
dd25432359 Bot API 6.0. Deprecation fixes 2022-04-23 15:03:54 +03:00
Badiboy
cfbbfe84ad Merge pull request #1501 from coder2020official/master
Prepare for Bot API 6.0
2022-04-23 14:10:26 +03:00
abdullaev388
b25d2846e9 TextFilter class supports case insensitiveness with lazy translations 2022-04-23 11:53:55 +05:00
abdullaev388
ab64e17464 added new i18n class based middleware 2022-04-23 11:48:58 +05:00
abdullaev388
3a5db47c1b removed old class based i18n middleware 2022-04-23 10:44:35 +05:00
coder2020official
4812dcb02b Fix typo in types.py 2022-04-22 23:06:11 +05:00
coder2020official
b146df346d Indentation fix to fit documentation. 2022-04-17 16:46:38 +05:00
coder2020official
5f2713bcfb Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-04-17 16:39:14 +05:00
coder2020official
a1bf961fd2 Bump Bot API 6.0(Beta) 2022-04-17 16:39:09 +05:00
_run
a8451a5e30 Merge branch 'eternnoir:master' into master 2022-04-17 14:07:44 +05:00
_run
43d0e10ba4 Merge branch 'eternnoir:master' into master 2022-04-16 23:52:41 +05:00
_run
9652fdbecb Update README.md 2022-04-16 23:50:45 +05:00
66 changed files with 2337 additions and 275 deletions

View File

@@ -10,6 +10,5 @@ OS:
## Checklist:
- [ ] I added/edited example on new feature/change (if exists)
- [ ] My changes won't break backend compatibility
- [ ] I made changes for async and sync
- [ ] My changes won't break backward compatibility
- [ ] I made changes both for sync and async

1
.gitignore vendored
View File

@@ -64,6 +64,7 @@ testMain.py
#VS Code
.vscode/
.DS_Store
*.code-workspace
# documentation
_build/

View File

@@ -11,7 +11,7 @@
<p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p>
<p align="center">Both synchronous and asynchronous.</p>
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#june-20-2022">6.1</a>!
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
@@ -65,7 +65,7 @@
* [Telegram Channel](#telegram-channel)
* [More examples](#more-examples)
* [Code Template](#code-template)
* [Bots using this API](#bots-using-this-api)
* [Bots using this library](#bots-using-this-library)
## Getting started
@@ -358,7 +358,25 @@ def start(message):
There are other examples using middleware handler in the [examples/middleware](examples/middleware) directory.
#### Class-based middlewares
There are class-based middlewares. Check out in [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/middleware/class_based)
There are class-based middlewares.
Basic class-based middleware looks like this:
```python
class Middleware(BaseMiddleware):
def __init__(self):
self.update_types = ['message']
def pre_process(self, message, data):
data['foo'] = 'Hello' # just for example
# we edited the data. now, this data is passed to handler.
# return SkipHandler() -> this will skip handler
# return CancelUpdate() -> this will cancel update
def post_process(self, message, data, exception=None):
print(data['foo'])
if exception: # check for exception
print(exception)
```
Class-based middleware should have to functions: post and pre process.
So, as you can see, class-based middlewares work before and after handler execution.
For more, check out in [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/middleware/class_based)
#### Custom filters
Also, you can use built-in custom filters. Or, you can create your own filter.
@@ -656,6 +674,8 @@ telebot.logger.setLevel(logging.DEBUG) # Outputs debug messages to console.
```
### Proxy
For sync:
You can use proxy for request. `apihelper.proxy` object will use by call `requests` proxies argument.
```python
@@ -670,6 +690,14 @@ If you want to use socket5 proxy you need install dependency `pip install reques
apihelper.proxy = {'https':'socks5://userproxy:password@proxy_address:port'}
```
For async:
```python
from telebot import asyncio_helper
asyncio_helper.proxy = 'http://127.0.0.1:3128' #url
```
### Testing
You can disable or change the interaction with real Telegram server by using
```python
@@ -699,7 +727,8 @@ Result will be:
## API conformance
* ✔ [Bot API 6.1](https://core.telegram.org/bots/api#june-20-2022)
* ✔ [Bot API 6.0](https://core.telegram.org/bots/api#april-16-2022)
* ✔ [Bot API 5.7](https://core.telegram.org/bots/api#january-31-2022)
* ✔ [Bot API 5.6](https://core.telegram.org/bots/api#december-30-2021)
* ✔ [Bot API 5.5](https://core.telegram.org/bots/api#december-7-2021)
@@ -816,7 +845,7 @@ Here are some examples of template:
* [TeleBot template](https://github.com/coder2020official/telebot_template)
## Bots using this API
## Bots using this library
* [SiteAlert bot](https://telegram.me/SiteAlert_bot) ([source](https://github.com/ilteoood/SiteAlert-Python)) by *ilteoood* - Monitors websites and sends a notification on changes
* [TelegramLoggingBot](https://github.com/aRandomStranger/TelegramLoggingBot) by *aRandomStranger*
* [Telegram LMGTFY_bot](https://github.com/GabrielRF/telegram-lmgtfy_bot) by *GabrielRF* - Let me Google that for you.
@@ -867,5 +896,8 @@ Here are some examples of template:
* [remoteTelegramShell](https://github.com/EnriqueMoran/remoteTelegramShell) by [EnriqueMoran](https://github.com/EnriqueMoran). Control your LinuxOS computer through Telegram.
* [Pyfram-telegram-bot](https://github.com/skelly37/pyfram-telegram-bot) Query wolframalpha.com and make use of its API through Telegram.
* [TranslateThisVideoBot](https://gitlab.com/WuerfelDev/translatethisvideo) This Bot can understand spoken text in videos and translate it to English
* [Zyprexa](https://t.me/mathemathicsBot) ([source](https://github.com/atif5/zyprexa)) Zyprexa can solve, help you solve any mathematical problem you encounter and convert your regular mathematical expressions into beautiful imagery using LaTeX.
* [Bincode-telegram-bot](https://github.com/tusharhero/bincode-telegram-bot) by [tusharhero](https://github.com/tusharhero) - Makes [bincodes](https://github.com/tusharhero/bincode) from text provides and also converts them back to text.
* [hydrolib_bot](https://github.com/Mayson90/hydrolib_bot) Toolset for Hydrophilia tabletop game (game cards, rules, structure...).
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**

View File

@@ -2,6 +2,12 @@
AsyncTeleBot
====================
.. meta::
:description: Asynchronous pyTelegramBotAPI
:keywords: ptba, pytba, pyTelegramBotAPI, asynctelebot, documentation
AsyncTeleBot methods
--------------------

View File

@@ -3,6 +3,10 @@
Callback data factory
=====================
.. meta::
:description: Callback data factory in pyTelegramBotAPI
:keywords: ptba, pytba, pyTelegramBotAPI, callbackdatafactory, guide, callbackdata, factory
callback\_data file
-----------------------------

View File

@@ -17,12 +17,12 @@
# -- Project information -----------------------------------------------------
project = 'pyTelegramBotAPI'
project = 'pyTelegramBotAPI Documentation'
copyright = '2022, coder2020official'
author = 'coder2020official'
# The full version, including alpha/beta/rc tags
release = '4.4.1'
release = '4.6.0'
# -- General configuration ---------------------------------------------------

View File

@@ -0,0 +1,12 @@
==================
Formatting options
==================
.. meta::
:description: Formatting options in pyTelegramBotAPI
:keywords: html, markdown, parse_mode, formatting, ptba, pytba, pyTelegramBotAPI
.. automodule:: telebot.formatting
:members:
:undoc-members:
:show-inheritance:

View File

@@ -7,6 +7,11 @@
Welcome to pyTelegramBotAPI's documentation!
============================================
.. meta::
:description: Official documentation of pyTelegramBotAPI
:keywords: ptba, pytba, pyTelegramBotAPI, documentation, guide
=======
TeleBot
=======
@@ -49,6 +54,7 @@ Content
async_version/index
calldata
util
formatting

View File

@@ -2,6 +2,10 @@
Installation Guide
==================
.. meta::
:description: Installation of pyTelegramBotAPI
:keywords: ptba, pytba, pyTelegramBotAPI, installation, guide
Using PIP
----------

View File

@@ -3,6 +3,10 @@
Quick start
===========
.. meta::
:description: Quickstart guide
:keywords: ptba, pytba, pyTelegramBotAPI, quickstart, guide
Synchronous TeleBot
-------------------
.. literalinclude:: ../../examples/echo_bot.py

View File

@@ -2,6 +2,10 @@
TeleBot version
===============
.. meta::
:description: Synchronous pyTelegramBotAPI documentation
:keywords: ptba, pytba, pyTelegramBotAPI, methods, guide, files, sync
TeleBot methods
---------------
.. automodule:: telebot

View File

@@ -2,6 +2,10 @@
Utils
============
.. meta::
:description: Utils in pyTelegramBotAPI
:keywords: ptba, pytba, pyTelegramBotAPI, utils, guide
util file
-------------------

View File

@@ -23,8 +23,8 @@ async def my_chat_m(message: types.ChatMemberUpdated):
#content_Type_service is:
#'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
#'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
#'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
#'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
#'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
#'video_chat_participants_invited', 'message_auto_delete_timer_changed'
# this handler deletes service messages
@bot.message_handler(content_types=util.content_type_service)

View File

@@ -0,0 +1,54 @@
from telebot.async_telebot import AsyncTeleBot
from telebot import formatting
bot = AsyncTeleBot('token')
@bot.message_handler(commands=['start'])
async def start_message(message):
await bot.send_message(
message.chat.id,
# function which connects all strings
formatting.format_text(
formatting.mbold(message.from_user.first_name),
formatting.mitalic(message.from_user.first_name),
formatting.munderline(message.from_user.first_name),
formatting.mstrikethrough(message.from_user.first_name),
formatting.mcode(message.from_user.first_name),
separator=" " # separator separates all strings
),
parse_mode='MarkdownV2'
)
# just a bold text using markdownv2
await bot.send_message(
message.chat.id,
formatting.mbold(message.from_user.first_name),
parse_mode='MarkdownV2'
)
# html
await bot.send_message(
message.chat.id,
formatting.format_text(
formatting.hbold(message.from_user.first_name),
formatting.hitalic(message.from_user.first_name),
formatting.hunderline(message.from_user.first_name),
formatting.hstrikethrough(message.from_user.first_name),
formatting.hcode(message.from_user.first_name),
# hide_link is only for html
formatting.hide_link('https://telegra.ph/file/c158e3a6e2a26a160b253.jpg'),
separator=" "
),
parse_mode='HTML'
)
# just a bold text in html
await bot.send_message(
message.chat.id,
formatting.hbold(message.from_user.first_name),
parse_mode='HTML'
)
import asyncio
asyncio.run(bot.polling())

View File

@@ -56,7 +56,7 @@ import keyboards
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import TextMatchFilter, TextFilter
from i18n_base_midddleware import I18N
from i18n_base_middleware import I18N
from telebot.asyncio_storage.memory_storage import StateMemoryStorage

View File

@@ -0,0 +1,17 @@
You probably have seen bots which allow you to send them token of your bot and then handle updates providing some functionality for your bot.
<br>
This type of bots are called <b>multibots</b>. They are created using webhooks.
<br>
This is the example of simple multibot.<br>
In order to reproduce this example you need to have <b>domain and ssl connection</b>.
<br>
If you have, go to config.py and specify your data.
<br>
There is also file called <b>nginx_conf.conf</b>, we will use nginx as proxy-server and this file is example nginx.conf file.
<br>
Make sure that server_name and port are the same in both config and nginx_conf
<br>
(nginx_conf.conf IS NOT complete, you would probably use tools like certbot to add ssl connection to it)
<br>
Also, in this example I used dictionary as tokens storage, but in production you should use database so that you can re-set webhooks in case bot restarts.

View File

@@ -0,0 +1,6 @@
MAIN_BOT_TOKEN = "your_main_bot_token"
WEBHOOK_HOST = "your_domain.com"
WEBHOOK_PATH = "telegram_webhook"
WEBAPP_HOST = "0.0.0.0"
WEBAPP_PORT = 3500

View File

@@ -0,0 +1,15 @@
from telebot.async_telebot import AsyncTeleBot
from telebot import types
async def hello_handler(message: types.Message, bot: AsyncTeleBot):
await bot.send_message(message.chat.id, "Hi :)")
async def echo_handler(message: types.Message, bot: AsyncTeleBot):
await bot.send_message(message.chat.id, message.text)
def register_handlers(bot: AsyncTeleBot):
bot.register_message_handler(hello_handler, func=lambda message: message.text == 'Hello', pass_bot=True)
bot.register_message_handler(echo_handler, pass_bot=True)

View File

@@ -0,0 +1,56 @@
import asyncio
from aiohttp import web
from telebot import types, util
from telebot.async_telebot import AsyncTeleBot
from handlers import register_handlers
import config
main_bot = AsyncTeleBot(config.MAIN_BOT_TOKEN)
app = web.Application()
tokens = {config.MAIN_BOT_TOKEN: True}
async def webhook(request):
token = request.match_info.get('token')
if not tokens.get(token):
return web.Response(status=404)
if request.headers.get('content-type') != 'application/json':
return web.Response(status=403)
json_string = await request.json()
update = types.Update.de_json(json_string)
if token == main_bot.token:
await main_bot.process_new_updates([update])
return web.Response()
from_update_bot = AsyncTeleBot(token)
register_handlers(from_update_bot)
await from_update_bot.process_new_updates([update])
return web.Response()
app.router.add_post("/" + config.WEBHOOK_PATH + "/{token}", webhook)
@main_bot.message_handler(commands=['add_bot'])
async def add_bot(message: types.Message):
token = util.extract_arguments(message.text)
tokens[token] = True
new_bot = AsyncTeleBot(token)
await new_bot.delete_webhook()
await new_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{token}")
await new_bot.send_message(message.chat.id, "Webhook was set.")
async def main():
await main_bot.delete_webhook()
await main_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{config.MAIN_BOT_TOKEN}")
web.run_app(app, host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,8 @@
server {
server_name your_domain.com;
location /telegram_webhook/ {
proxy_pass http://localhost:3500;
}
}

View File

@@ -23,8 +23,8 @@ def my_chat_m(message: types.ChatMemberUpdated):
#content_Type_service is:
#'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
#'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
#'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
#'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
#'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
#'video_chat_participants_invited', 'message_auto_delete_timer_changed'
# this handler deletes service messages
@bot.message_handler(content_types=util.content_type_service)

View File

@@ -0,0 +1,33 @@
import telebot
from time import sleep, time
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton #Only for creating Inline Buttons, not necessary for creating Invite Links
Token = "api_token" #Your Bot Access Token
Group_ID = -1234567890 #Group ID for which invite link is to be created
bot = telebot.TeleBot(Token, parse_mode="HTML")
#/start command message
@bot.message_handler(commands=['start'])
def startmsg(msg):
bot.reply_to(msg, "Hey there, I'm a bot made by pyTelegramBotAPI!")
#Get notified of incoming members in group
@bot.message_handler(content_types=['new_chat_members'])
def newmember(msg):
#Create an invite link class that contains info about the created invite link using create_chat_invite_link() with parameters
invite = bot.create_chat_invite_link(Group_ID, member_limit=1, expire_date=int(time())+45) #Here, the link will auto-expire in 45 seconds
InviteLink = invite.invite_link #Get the actual invite link from 'invite' class
mrkplink = InlineKeyboardMarkup() #Created Inline Keyboard Markup
mrkplink.add(InlineKeyboardButton("Join our group 🚀", url=InviteLink)) #Added Invite Link to Inline Keyboard
bot.send_message(msg.chat.id, f"Hey there {msg.from_user.first_name}, Click the link below to join our Official Group.", reply_markup=mrkplink)
#This will send a message with the newly-created invite link as markup button.
#The member limit will be 1 and expiring time will be 45 sec.
bot.infinity_polling()

View File

@@ -60,7 +60,7 @@ def name_get(message):
"""
State 1. Will process when user's state is MyStates.name.
"""
bot.send_message(message.chat.id, f'Now write me a surname')
bot.send_message(message.chat.id, 'Now write me a surname')
bot.set_state(message.from_user.id, MyStates.surname, message.chat.id)
with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
data['name'] = message.text
@@ -83,7 +83,11 @@ def ready_for_answer(message):
State 3. Will process when user's state is MyStates.age.
"""
with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
bot.send_message(message.chat.id, "Ready, take a look:\n<b>Name: {name}\nSurname: {surname}\nAge: {age}</b>".format(name=data['name'], surname=data['surname'], age=message.text), parse_mode="html")
msg = ("Ready, take a look:\n<b>"
f"Name: {data['name']}\n"
f"Surname: {data['surname']}\n"
f"Age: {message.text}</b>")
bot.send_message(message.chat.id, msg, parse_mode="html")
bot.delete_state(message.from_user.id, message.chat.id)
#incorrect number
@@ -99,4 +103,4 @@ def age_incorrect(message):
bot.add_custom_filter(custom_filters.StateFilter(bot))
bot.add_custom_filter(custom_filters.IsDigitFilter())
bot.infinity_polling(skip_pending=True)
bot.infinity_polling(skip_pending=True)

View File

@@ -0,0 +1,53 @@
from telebot import TeleBot
from telebot import formatting
bot = TeleBot('TOKEN')
@bot.message_handler(commands=['start'])
def start_message(message):
bot.send_message(
message.chat.id,
# function which connects all strings
formatting.format_text(
formatting.mbold(message.from_user.first_name),
formatting.mitalic(message.from_user.first_name),
formatting.munderline(message.from_user.first_name),
formatting.mstrikethrough(message.from_user.first_name),
formatting.mcode(message.from_user.first_name),
separator=" " # separator separates all strings
),
parse_mode='MarkdownV2'
)
# just a bold text using markdownv2
bot.send_message(
message.chat.id,
formatting.mbold(message.from_user.first_name),
parse_mode='MarkdownV2'
)
# html
bot.send_message(
message.chat.id,
formatting.format_text(
formatting.hbold(message.from_user.first_name),
formatting.hitalic(message.from_user.first_name),
formatting.hunderline(message.from_user.first_name),
formatting.hstrikethrough(message.from_user.first_name),
formatting.hcode(message.from_user.first_name),
# hide_link is only for html
formatting.hide_link('https://telegra.ph/file/c158e3a6e2a26a160b253.jpg'),
separator=" "
),
parse_mode='HTML'
)
# just a bold text in html
bot.send_message(
message.chat.id,
formatting.hbold(message.from_user.first_name),
parse_mode='HTML'
)
bot.infinity_polling()

View File

@@ -0,0 +1,121 @@
import gettext
import os
import threading
from telebot.handler_backends import BaseMiddleware
try:
from babel.support import LazyProxy
babel_imported = True
except ImportError:
babel_imported = False
class I18N(BaseMiddleware):
"""
This middleware provides high-level tool for internationalization
It is based on gettext util.
"""
context_lang = threading.local()
def __init__(self, translations_path, domain_name: str):
super().__init__()
self.update_types = self.process_update_types()
self.path = translations_path
self.domain = domain_name
self.translations = self.find_translations()
@property
def available_translations(self):
return list(self.translations)
def gettext(self, text: str, lang: str = None):
"""
Singular translations
"""
if lang is None:
lang = self.context_lang.language
if lang not in self.translations:
return text
translator = self.translations[lang]
return translator.gettext(text)
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
"""
Plural translations
"""
if lang is None:
lang = self.context_lang.language
if lang not in self.translations:
if n == 1:
return singular
return plural
translator = self.translations[lang]
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, text: str, lang: str = None):
if not babel_imported:
raise RuntimeError('babel module is not imported. Check that you installed it.')
return LazyProxy(self.gettext, text, lang, enable_cache=False)
def lazy_ngettext(self, singular: str, plural: str, lang: str = None, n=1):
if not babel_imported:
raise RuntimeError('babel module is not imported. Check that you installed it.')
return LazyProxy(self.ngettext, singular, plural, lang, n, enable_cache=False)
def get_user_language(self, obj):
"""
You need to override this method and return user language
"""
raise NotImplementedError
def process_update_types(self) -> list:
"""
You need to override this method and return any update types which you want to be processed
"""
raise NotImplementedError
def pre_process(self, message, data):
"""
context language variable will be set each time when update from 'process_update_types' comes
value is the result of 'get_user_language' method
"""
self.context_lang.language = self.get_user_language(obj=message)
def post_process(self, message, data, exception):
pass
def find_translations(self):
"""
Looks for translations with passed 'domain' in passed 'path'
"""
if not os.path.exists(self.path):
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
result = {}
for name in os.listdir(self.path):
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
if not os.path.isdir(translations_path):
continue
po_file = os.path.join(translations_path, self.domain + '.po')
mo_file = po_file[:-2] + 'mo'
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
with open(mo_file, 'rb') as file:
result[name] = gettext.GNUTranslations(file)
return result

View File

@@ -0,0 +1,34 @@
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
def languages_keyboard():
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text="English", callback_data='en'),
InlineKeyboardButton(text="Русский", callback_data='ru'),
InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn')
]
]
)
def clicker_keyboard(_):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click"), callback_data='click'),
]
]
)
def menu_keyboard(_):
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
keyboard.add(
KeyboardButton(text=_("My user id")),
KeyboardButton(text=_("My user name")),
KeyboardButton(text=_("My first name"))
)
return keyboard

View File

@@ -0,0 +1,81 @@
# English translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr ""
#: keyboards.py:29
msgid "My user id"
msgstr ""
#: keyboards.py:30
msgid "My user name"
msgstr ""
#: keyboards.py:31
msgid "My first name"
msgstr ""
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
#: main.py:121
msgid "Language has been changed"
msgstr ""
#: main.py:130 main.py:150
#, fuzzy
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] ""
msgstr[1] ""
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr ""
#: main.py:203
msgid "Seems you confused language"
msgstr ""
#~ msgid ""
#~ "Hello, {user_fist_name}!\n"
#~ "This is the example of multilanguage bot.\n"
#~ "Available commands:\n"
#~ "\n"
#~ "/lang - change your language\n"
#~ "/plural - pluralization example"
#~ msgstr ""

View File

@@ -0,0 +1,82 @@
# Russian translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "Клик"
#: keyboards.py:29
msgid "My user id"
msgstr "Мой user id"
#: keyboards.py:30
msgid "My user name"
msgstr "Мой user name"
#: keyboards.py:31
msgid "My first name"
msgstr "Мой first name"
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
"Привет, {user_fist_name}!\n"
"Это пример мультиязычного бота.\n"
"Доступные команды:\n"
"\n"
"/lang - изменить язык\n"
"/plural - пример плюрализации\n"
"/menu - Пример текстового меню"
#: main.py:121
msgid "Language has been changed"
msgstr "Язык был сменён"
#: main.py:130 main.py:150
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "У вас {number} клик"
msgstr[1] "У вас {number} клика"
msgstr[2] "У вас {number} кликов"
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Это кликер.\n"
"\n"
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr "Это пример ReplyKeyboardMarkup меню в мультиязычном боте."
#: main.py:203
msgid "Seems you confused language"
msgstr "Кажется, вы перепутали язык"

View File

@@ -0,0 +1,80 @@
# Uzbek (Latin) translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: uz_Latn\n"
"Language-Team: uz_Latn <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "clik"
#: keyboards.py:29
msgid "My user id"
msgstr "Mani user id"
#: keyboards.py:30
msgid "My user name"
msgstr "Mani user name"
#: keyboards.py:31
msgid "My first name"
msgstr "Mani first name"
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
"Salom, {user_fist_name}!\n"
"Bu multilanguage bot misoli.\n"
"Mavjud buyruqlar:\n"
"\n"
"/lang - tilni ozgartirish\n"
"/plural - pluralizatsiya misoli\n"
"/menu - text menu misoli"
#: main.py:121
msgid "Language has been changed"
msgstr "Til ozgartirildi"
#: main.py:130 main.py:150
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "Sizda {number}ta clik"
msgstr[1] "Sizda {number}ta clik"
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Bu clicker.\n"
"\n"
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr "Bu multilanguage bot da replykeyboardmarkup menyu misoli."
#: main.py:203
msgid "Seems you confused language"
msgstr "Tilni adashtirdiz"

View File

@@ -0,0 +1,211 @@
"""
In this example you will learn how to adapt your bot to different languages
Using built-in middleware I18N.
You need to install babel package 'https://pypi.org/project/Babel/'
Babel provides a command-line interface for working with message catalogs
After installing babel package you have a script called 'pybabel'
Too see all the commands open terminal and type 'pybabel --help'
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
Create a directory 'locales' where our translations will be stored
First we need to extract texts:
pybabel extract -o locales/{domain_name}.pot --input-dirs .
{domain_name}.pot - is the file where all translations are saved
The name of this file should be the same as domain which you pass to I18N class
In this example domain_name will be 'messages'
For gettext (singular texts) we use '_' alias and it works perfect
You may also you some alias for ngettext (plural texts) but you can face with a problem that
your plural texts are not being extracted
That is because by default 'pybabel extract' recognizes the following keywords:
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
To add your own keyword you can use '-k' flag
In this example for 'ngettext' i will assign double underscore alias '__'
Full command with pluralization support will look so:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
pybabel init -i locales/{domain_name}.pot -d locales -l en
pybabel init -i locales/{domain_name}.pot -d locales -l ru
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
After you translated all the texts you need to compile .po files:
pybabel compile -d locales
When you delete/update your texts you also need to update them in .po files:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
pybabel update -i locales/{domain_name}.pot -d locales
- translate
pybabel compile -d locales
If you have any exceptions check:
- you have installed babel
- translations are ready, so you just compiled it
- in the commands above you replaced {domain_name} to messages
- you are writing commands from correct path in terminal
"""
import asyncio
from typing import Union
import keyboards
from i18n_base_middleware import I18N
from telebot import TeleBot
from telebot import types, StateMemoryStorage
from telebot.custom_filters import TextMatchFilter, TextFilter
class I18NMiddleware(I18N):
def process_update_types(self) -> list:
"""
Here you need to return a list of update types which you want to be processed
"""
return ['message', 'callback_query']
def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]):
"""
This method is called when new update comes (only updates which you return in 'process_update_types' method)
Returned language will be used in 'pre_process' method of parent class
Returned language will be set to context language variable.
If you need to get translation with user's actual language you don't have to pass it manually
It will be automatically passed from context language value.
However if you need some other language you can always pass it.
"""
user_id = obj.from_user.id
if user_id not in users_lang:
users_lang[user_id] = 'en'
return users_lang[user_id]
storage = StateMemoryStorage()
bot = TeleBot("", state_storage=storage, use_class_middlewares=True)
i18n = I18NMiddleware(translations_path='locales', domain_name='messages')
_ = i18n.gettext # for singular translations
__ = i18n.ngettext # for plural translations
# These are example storages, do not use it in a production development
users_lang = {}
users_clicks = {}
@bot.message_handler(commands=['start'])
def start_handler(message: types.Message):
text = _("Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example")
# remember don't use f string for interpolation, use .format method instead
text = text.format(user_fist_name=message.from_user.first_name)
bot.send_message(message.from_user.id, text)
@bot.message_handler(commands=['lang'])
def change_language_handler(message: types.Message):
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
reply_markup=keyboards.languages_keyboard())
@bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn']))
def language_handler(call: types.CallbackQuery):
lang = call.data
users_lang[call.from_user.id] = lang
# When you changed user language, you have to pass it manually beacause it is not changed in context
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
@bot.message_handler(commands=['plural'])
def pluralization_handler(message: types.Message):
if not users_clicks.get(message.from_user.id):
users_clicks[message.from_user.id] = 0
clicks = users_clicks[message.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks
)
text = _("This is clicker.\n\n") + text.format(number=clicks)
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
def click_handler(call: types.CallbackQuery):
if not users_clicks.get(call.from_user.id):
users_clicks[call.from_user.id] = 1
else:
users_clicks[call.from_user.id] += 1
clicks = users_clicks[call.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks
)
text = _("This is clicker.\n\n") + text.format(number=clicks)
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_))
@bot.message_handler(commands=['menu'])
def menu_handler(message: types.Message):
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_))
# For lazy tranlations
# lazy gettext is used when you don't know user's locale
# It can be used for example to handle text buttons in multilanguage bot
# The actual translation will be delayed until update comes and context language is set
l_ = i18n.lazy_gettext
# Handlers below will handle text according to user's language
@bot.message_handler(text=l_("My user id"))
def return_user_id(message: types.Message):
bot.send_message(message.chat.id, str(message.from_user.id))
@bot.message_handler(text=l_("My user name"))
def return_user_id(message: types.Message):
username = message.from_user.username
if not username:
username = '-'
bot.send_message(message.chat.id, username)
# You can make it case-insensitive
@bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True))
def return_user_id(message: types.Message):
bot.send_message(message.chat.id, message.from_user.first_name)
all_menu_texts = []
for language in i18n.available_translations:
for menu_text in ("My user id", "My user name", "My first name"):
all_menu_texts.append(_(menu_text, language))
# When user confused language. (handles all menu buttons texts)
@bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True))
def missed_message(message: types.Message):
bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_))
if __name__ == '__main__':
bot.setup_middleware(i18n)
bot.add_custom_filter(TextMatchFilter())
asyncio.run(bot.infinity_polling())

View File

@@ -1,5 +1,6 @@
import gettext
import os
import threading
class I18N:
@@ -8,6 +9,8 @@ class I18N:
It is based on gettext util.
"""
context_lang = threading.local()
def __init__(self, translations_path, domain_name: str):
self.path = translations_path
self.domain = domain_name
@@ -21,7 +24,11 @@ class I18N:
"""
Singular translations
"""
if not lang or lang not in self.translations:
if lang is None:
lang = self.context_lang.language
if lang not in self.translations:
return text
translator = self.translations[lang]
@@ -31,7 +38,10 @@ class I18N:
"""
Plural translations
"""
if not lang or lang not in self.translations:
if lang is None:
lang = self.context_lang.language
if lang not in self.translations:
if n == 1:
return singular
return plural
@@ -39,6 +49,7 @@ class I18N:
translator = self.translations[lang]
return translator.ngettext(singular, plural, n)
def find_translations(self):
"""
Looks for translations with passed 'domain' in passed 'path'

View File

@@ -13,11 +13,11 @@ def languages_keyboard():
)
def clicker_keyboard(_, lang):
def clicker_keyboard(_):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
InlineKeyboardButton(text=_("click"), callback_data='click'),
]
]
)

View File

@@ -47,5 +47,4 @@ msgstr[1] ""
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
msgstr ""

View File

@@ -1,21 +1,17 @@
"""
In this example you will learn how to adapt your bot to different languages
Using built-in class I18N.
You need to install babel package 'https://pypi.org/project/Babel/'
Babel provides a command-line interface for working with message catalogs
After installing babel package you have a script called 'pybabel'
Too see all the commands open terminal and type 'pybabel --help'
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
Create a directory 'locales' where our translations will be stored
First we need to extract texts:
pybabel extract -o locales/{domain_name}.pot --input-dirs .
{domain_name}.pot - is the file where all translations are saved
The name of this file should be the same as domain which you pass to I18N class
In this example domain_name will be 'messages'
For gettext (singular texts) we use '_' alias and it works perfect
You may also you some alias for ngettext (plural texts) but you can face with a problem that
your plural texts are not being extracted
@@ -23,25 +19,20 @@ That is because by default 'pybabel extract' recognizes the following keywords:
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
To add your own keyword you can use '-k' flag
In this example for 'ngettext' i will assign double underscore alias '__'
Full command with pluralization support will look so:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
pybabel init -i locales/{domain_name}.pot -d locales -l en
pybabel init -i locales/{domain_name}.pot -d locales -l ru
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
After you translated all the texts you need to compile .po files:
pybabel compile -d locales
When you delete/update your texts you also need to update them in .po files:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
pybabel update -i locales/{domain_name}.pot -d locales
- translate
pybabel compile -d locales
If you have any exceptions check:
- you have installed babel
- translations are ready, so you just compiled it
@@ -49,14 +40,17 @@ If you have any exceptions check:
- you are writing commands from correct path in terminal
"""
from functools import wraps
import keyboards
from telebot import TeleBot, types, custom_filters
from telebot import apihelper
from telebot.storage.memory_storage import StateMemoryStorage
import keyboards
from i18n_class import I18N
apihelper.ENABLE_MIDDLEWARE = True
storage = StateMemoryStorage()
bot = TeleBot("", state_storage=storage)
# IMPORTANT! This example works only if polling is non-threaded.
bot = TeleBot("", state_storage=storage, threaded=False)
i18n = I18N(translations_path='locales', domain_name='messages')
_ = i18n.gettext # for singular translations
@@ -67,35 +61,25 @@ users_lang = {}
users_clicks = {}
def get_user_language(func):
"""
This decorator will pass to your handler current user's language
"""
@wraps(func)
def inner(*args, **kwargs):
obj = args[0]
kwargs.update(lang=users_lang.get(obj.from_user.id, 'en'))
return func(*args, **kwargs)
return inner
@bot.middleware_handler(update_types=['message', 'callback_query'])
def set_contex_language(bot_instance, message):
i18n.context_lang.language = users_lang.get(message.from_user.id, 'en')
@bot.message_handler(commands='start')
@get_user_language
def start_handler(message: types.Message, lang):
@bot.message_handler(commands=['start'])
def start_handler(message: types.Message):
text = _("Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n\n"
"/lang - change your language\n"
"/plural - pluralization example", lang=lang)
"/plural - pluralization example")
# remember don't use f string for interpolation, use .format method instead
text = text.format(user_fist_name=message.from_user.first_name)
bot.send_message(message.from_user.id, text)
@bot.message_handler(commands='lang')
@bot.message_handler(commands=['lang'])
def change_language_handler(message: types.Message):
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
reply_markup=keyboards.languages_keyboard())
@@ -106,13 +90,13 @@ def language_handler(call: types.CallbackQuery):
lang = call.data
users_lang[call.from_user.id] = lang
# When you change user's language, pass language explicitly coz it's not changed in context
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
bot.delete_state(call.from_user.id)
@bot.message_handler(commands='plural')
@get_user_language
def pluralization_handler(message: types.Message, lang):
@bot.message_handler(commands=['plural'])
def pluralization_handler(message: types.Message):
if not users_clicks.get(message.from_user.id):
users_clicks[message.from_user.id] = 0
clicks = users_clicks[message.from_user.id]
@@ -121,15 +105,13 @@ def pluralization_handler(message: types.Message, lang):
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks,
lang=lang
)
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_, lang))
text = _("This is clicker.\n\n") + text.format(number=clicks)
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
@get_user_language
def click_handler(call: types.CallbackQuery, lang):
def click_handler(call: types.CallbackQuery):
if not users_clicks.get(call.from_user.id):
users_clicks[call.from_user.id] = 1
else:
@@ -140,12 +122,11 @@ def click_handler(call: types.CallbackQuery, lang):
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks,
lang=lang
n=clicks
)
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
text = _("This is clicker.\n\n") + text.format(number=clicks)
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_, lang))
reply_markup=keyboards.clicker_keyboard(_))
if __name__ == '__main__':

View File

@@ -0,0 +1,17 @@
You probably have seen bots which allow you to send them token of your bot and then handle updates providing some functionality for your bot.
<br>
This type of bots are called <b>multibots</b>. They are created using webhooks.
<br>
This is the example of simple multibot.<br>
In order to reproduce this example you need to have <b>domain and ssl connection</b>.
<br>
If you have, go to config.py and specify your data.
<br>
There is also file called <b>nginx_conf.conf</b>, we will use nginx as proxy-server and this file is example nginx.conf file.
<br>
Make sure that server_name and port are the same in both config and nginx_conf
<br>
(nginx_conf.conf IS NOT complete, you would probably use tools like certbot to add ssl connection to it)
<br>
Also, in this example I used dictionary as tokens storage, but in production you should use database so that you can re-set webhooks in case bot restarts.

View File

@@ -0,0 +1,6 @@
MAIN_BOT_TOKEN = "your_main_bot_token"
WEBHOOK_HOST = "your_domain.com"
WEBHOOK_PATH = "telegram_webhook"
WEBAPP_HOST = "0.0.0.0"
WEBAPP_PORT = 3500

View File

@@ -0,0 +1,14 @@
from telebot import types, TeleBot
def hello_handler(message: types.Message, bot: TeleBot):
bot.send_message(message.chat.id, "Hi :)")
def echo_handler(message: types.Message, bot: TeleBot):
bot.send_message(message.chat.id, message.text)
def register_handlers(bot: TeleBot):
bot.register_message_handler(hello_handler, func=lambda message: message.text == 'Hello', pass_bot=True)
bot.register_message_handler(echo_handler, pass_bot=True)

48
examples/multibot/main.py Normal file
View File

@@ -0,0 +1,48 @@
from flask import Flask
from flask import request, abort
from telebot import TeleBot, types, util
from handlers import register_handlers
import config
main_bot = TeleBot(config.MAIN_BOT_TOKEN)
app = Flask(__name__)
tokens = {config.MAIN_BOT_TOKEN: True}
@app.route(f"/{config.WEBHOOK_PATH}/<token>", methods=['POST'])
def webhook(token: str):
if not tokens.get(token):
return abort(404)
if request.headers.get('content-type') != 'application/json':
return abort(403)
json_string = request.get_data().decode('utf-8')
update = types.Update.de_json(json_string)
if token == main_bot.token:
main_bot.process_new_updates([update])
return ''
from_update_bot = TeleBot(token)
register_handlers(from_update_bot)
from_update_bot.process_new_updates([update])
return ''
@main_bot.message_handler(commands=['add_bot'])
def add_bot(message: types.Message):
token = util.extract_arguments(message.text)
tokens[token] = True
new_bot = TeleBot(token)
new_bot.delete_webhook()
new_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{token}")
new_bot.send_message(message.chat.id, "Webhook was set.")
if __name__ == '__main__':
main_bot.delete_webhook()
main_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{config.MAIN_BOT_TOKEN}")
app.run(host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)

View File

@@ -0,0 +1,8 @@
server {
server_name your_domain.com;
location /telegram_webhook/ {
proxy_pass http://localhost:3500;
}
}

View File

@@ -38,21 +38,20 @@ def command_pay(message):
"Real cards won't work with me, no money will be debited from your account."
" Use this test card number to pay for your Time Machine: `4242 4242 4242 4242`"
"\n\nThis is your demo invoice:", parse_mode='Markdown')
bot.send_invoice(message.chat.id, title='Working Time Machine',
description='Want to visit your great-great-great-grandparents?'
' Make a fortune at the races?'
' Shake hands with Hammurabi and take a stroll in the Hanging Gardens?'
' Order our Working Time Machine today!',
provider_token=provider_token,
currency='usd',
bot.send_invoice(
message.chat.id, #chat_id
'Working Time Machine', #title
' Want to visit your great-great-great-grandparents? Make a fortune at the races? Shake hands with Hammurabi and take a stroll in the Hanging Gardens? Order our Working Time Machine today!', #description
'HAPPY FRIDAYS COUPON', #invoice_payload
provider_token, #provider_token
'usd', #currency
prices, #prices
photo_url='http://erkelzaar.tsudao.com/models/perrotta/TIME_MACHINE.jpg',
photo_height=512, # !=0/None or picture won't be shown
photo_width=512,
photo_size=512,
is_flexible=False, # True If you need to set up Shipping Fee
prices=prices,
start_parameter='time-machine-example',
invoice_payload='HAPPY FRIDAYS COUPON')
start_parameter='time-machine-example')
@bot.shipping_query_handler(func=lambda query: True)

View File

@@ -1,4 +1,4 @@
pytest
requests==2.20.0
wheel==0.24.0
aiohttp>=3.8.0,<3.9.0
aiohttp>=3.8.0,<3.9.0

View File

@@ -26,7 +26,8 @@ setup(name='pyTelegramBotAPI',
extras_require={
'json': 'ujson',
'PIL': 'Pillow',
'redis': 'redis>=3.4.1'
'redis': 'redis>=3.4.1',
'aiohttp': 'aiohttp',
},
classifiers=[
'Development Status :: 5 - Production/Stable',

View File

@@ -9,6 +9,7 @@ import time
import traceback
from typing import Any, Callable, List, Optional, Union
# these imports are used to avoid circular import error
import telebot.util
import telebot.types
@@ -187,8 +188,6 @@ class TeleBot:
compatibility whose purpose was to enable file saving capability for handlers. And the same
implementation is now available with FileHandlerBackend
Most probably this function should be deprecated in future major releases
:param delay: Delay between changes in handlers and saving
:param filename: Filename of save file
"""
@@ -211,8 +210,6 @@ class TeleBot:
compatibility whose purpose was to enable file saving capability for handlers. And the same
implementation is now available with FileHandlerBackend
Most probably this function should be deprecated in future major releases
:param delay: Delay between changes in handlers and saving
:param filename: Filename of save file
"""
@@ -225,8 +222,6 @@ class TeleBot:
This function is left to keep backward compatibility whose purpose was to disable file saving capability
for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new next_step_backend backend
instead of FileHandlerBackend.
Most probably this function should be deprecated in future major releases
"""
self.next_step_backend = MemoryHandlerBackend(self.next_step_backend.handlers)
@@ -237,8 +232,6 @@ class TeleBot:
This function is left to keep backward compatibility whose purpose was to disable file saving capability
for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new reply_backend backend
instead of FileHandlerBackend.
Most probably this function should be deprecated in future major releases
"""
self.reply_backend = MemoryHandlerBackend(self.reply_backend.handlers)
@@ -250,8 +243,6 @@ class TeleBot:
help of FileHandlerBackend and is only recommended to use if next_step_backend was assigned as
FileHandlerBackend before entering this function
Most probably this function should be deprecated in future major releases
:param filename: Filename of the file where handlers was saved
:param del_file_after_loading: Is passed True, after loading save file will be deleted
"""
@@ -265,15 +256,13 @@ class TeleBot:
help of FileHandlerBackend and is only recommended to use if reply_backend was assigned as
FileHandlerBackend before entering this function
Most probably this function should be deprecated in future major releases
:param filename: Filename of the file where handlers was saved
:param del_file_after_loading: Is passed True, after loading save file will be deleted
"""
self.reply_backend.load_handlers(filename, del_file_after_loading)
def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
drop_pending_updates = None, timeout=None):
drop_pending_updates = None, timeout=None, secret_token=None):
"""
Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an
update for the bot, we will send an HTTPS POST request to the specified url,
@@ -298,10 +287,11 @@ class TeleBot:
resolved through DNS
:param drop_pending_updates: Pass True to drop all pending updates
:param timeout: Integer. Request connection timeout
:param secret_token: Secret token to be used to verify the webhook request.
:return: API reply.
"""
return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address,
drop_pending_updates, timeout)
drop_pending_updates, timeout, secret_token)
def delete_webhook(self, drop_pending_updates=None, timeout=None):
"""
@@ -396,7 +386,7 @@ class TeleBot:
new_poll_answers = None
new_my_chat_members = None
new_chat_members = None
chat_join_request = None
new_chat_join_request = None
for update in updates:
if apihelper.ENABLE_MIDDLEWARE:
@@ -453,8 +443,8 @@ class TeleBot:
if new_chat_members is None: new_chat_members = []
new_chat_members.append(update.chat_member)
if update.chat_join_request:
if chat_join_request is None: chat_join_request = []
chat_join_request.append(update.chat_join_request)
if new_chat_join_request is None: new_chat_join_request = []
new_chat_join_request.append(update.chat_join_request)
if new_messages:
self.process_new_messages(new_messages)
@@ -482,8 +472,8 @@ class TeleBot:
self.process_new_my_chat_member(new_my_chat_members)
if new_chat_members:
self.process_new_chat_member(new_chat_members)
if chat_join_request:
self.process_new_chat_join_request(chat_join_request)
if new_chat_join_request:
self.process_new_chat_join_request(new_chat_join_request)
def process_new_messages(self, new_messages):
self._notify_next_handlers(new_messages)
@@ -554,15 +544,15 @@ class TeleBot:
for listener in self.update_listener:
self._exec_task(listener, new_messages)
def infinity_polling(self, timeout: int=20, skip_pending: bool=False, long_polling_timeout: int=20, logger_level=logging.ERROR,
allowed_updates: Optional[List[str]]=None, *args, **kwargs):
def infinity_polling(self, timeout: int=20, skip_pending: bool=False, long_polling_timeout: int=20,
logger_level=logging.ERROR, allowed_updates: Optional[List[str]]=None, *args, **kwargs):
"""
Wrap polling with infinite loop and exception handling to avoid bot stops polling.
:param timeout: Request connection timeout
:param long_polling_timeout: Timeout in seconds for long polling (see API docs)
:param skip_pending: skip old updates
:param logger_level: Custom logging level for infinity_polling logging.
:param logger_level: Custom (different from logger itself) logging level for infinity_polling logging.
Use logger levels from logging as a value. None/NOTSET = no error logging
:param allowed_updates: A list of the update types you want your bot to receive.
For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types.
@@ -578,8 +568,8 @@ class TeleBot:
while not self.__stop_polling.is_set():
try:
self.polling(none_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout,
allowed_updates=allowed_updates, *args, **kwargs)
self.polling(non_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout,
logger_level=logger_level, allowed_updates=allowed_updates, *args, **kwargs)
except Exception as e:
if logger_level and logger_level >= logging.ERROR:
logger.error("Infinity polling exception: %s", str(e))
@@ -592,9 +582,9 @@ class TeleBot:
if logger_level and logger_level >= logging.INFO:
logger.error("Break infinity polling")
def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20,
long_polling_timeout: int=20, allowed_updates: Optional[List[str]]=None,
none_stop: Optional[bool]=None):
def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20, long_polling_timeout: int=20,
logger_level=logging.ERROR, allowed_updates: Optional[List[str]]=None,
none_stop: Optional[bool]=None):
"""
This function creates a new Thread that calls an internal __retrieve_updates function.
This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly.
@@ -608,6 +598,8 @@ class TeleBot:
:param timeout: Request connection timeout
:param skip_pending: skip old updates
:param long_polling_timeout: Timeout in seconds for long polling (see API docs)
:param logger_level: Custom (different from logger itself) logging level for infinity_polling logging.
Use logger levels from logging as a value. None/NOTSET = no error logging
:param allowed_updates: A list of the update types you want your bot to receive.
For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types.
See util.update_types for a complete list of available update types.
@@ -620,18 +612,27 @@ class TeleBot:
:return:
"""
if none_stop is not None:
logger.warning("polling: none_stop parameter is deprecated. Use non_stop instead.")
non_stop = none_stop
if skip_pending:
self.__skip_updates()
if self.threaded:
self.__threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates)
self.__threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout,
logger_level=logger_level, allowed_updates=allowed_updates)
else:
self.__non_threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates)
self.__non_threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout,
logger_level=logger_level, allowed_updates=allowed_updates)
def __threaded_polling(self, non_stop=False, interval=0, timeout = None, long_polling_timeout = None, allowed_updates=None):
logger.info('Started polling.')
def __threaded_polling(self, non_stop = False, interval = 0, timeout = None, long_polling_timeout = None,
logger_level=logging.ERROR, allowed_updates=None):
if not(logger_level) or (logger_level < logging.INFO):
warning = "\n Warning: this message appearance will be changed. Set logger_level=logging.INFO to continue seeing it."
else:
warning = ""
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info('Started polling.' + warning)
self.__stop_polling.clear()
error_interval = 0.25
@@ -656,14 +657,17 @@ class TeleBot:
else:
handled = False
if not handled:
logger.error(e)
if logger_level and logger_level >= logging.ERROR:
logger.error("Threaded polling exception: %s", str(e))
if logger_level and logger_level >= logging.DEBUG:
logger.error("Exception traceback:\n%s", traceback.format_exc())
if not non_stop:
self.__stop_polling.set()
logger.info("Exception occurred. Stopping.")
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("Exception occurred. Stopping." + warning)
else:
# polling_thread.clear_exceptions()
# self.worker_pool.clear_exceptions()
logger.info("Waiting for {0} seconds until retry".format(error_interval))
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("Waiting for {0} seconds until retry".format(error_interval) + warning)
time.sleep(error_interval)
if error_interval * 2 < 60:
error_interval *= 2
@@ -676,7 +680,8 @@ class TeleBot:
polling_thread.clear_exceptions() #*
self.worker_pool.clear_exceptions() #*
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received.")
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("KeyboardInterrupt received." + warning)
self.__stop_polling.set()
break
except Exception as e:
@@ -695,12 +700,19 @@ class TeleBot:
time.sleep(error_interval)
polling_thread.stop()
polling_thread.clear_exceptions() #*
self.worker_pool.clear_exceptions() #*
logger.info('Stopped polling.')
polling_thread.clear_exceptions()
self.worker_pool.clear_exceptions()
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info('Stopped polling.' + warning)
def __non_threaded_polling(self, non_stop=False, interval=0, timeout=None, long_polling_timeout=None, allowed_updates=None):
logger.info('Started polling.')
def __non_threaded_polling(self, non_stop=False, interval=0, timeout=None, long_polling_timeout=None,
logger_level=logging.ERROR, allowed_updates=None):
if not(logger_level) or (logger_level < logging.INFO):
warning = "\n Warning: this message appearance will be changed. Set logger_level=logging.INFO to continue seeing it."
else:
warning = ""
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info('Started polling.' + warning)
self.__stop_polling.clear()
error_interval = 0.25
@@ -715,18 +727,24 @@ class TeleBot:
handled = False
if not handled:
logger.error(e)
if logger_level and logger_level >= logging.ERROR:
logger.error("Polling exception: %s", str(e))
if logger_level and logger_level >= logging.DEBUG:
logger.error("Exception traceback:\n%s", traceback.format_exc())
if not non_stop:
self.__stop_polling.set()
logger.info("Exception occurred. Stopping.")
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("Exception occurred. Stopping." + warning)
else:
logger.info("Waiting for {0} seconds until retry".format(error_interval))
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("Waiting for {0} seconds until retry".format(error_interval) + warning)
time.sleep(error_interval)
error_interval *= 2
else:
time.sleep(error_interval)
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received.")
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info("KeyboardInterrupt received." + warning)
self.__stop_polling.set()
break
except Exception as e:
@@ -738,8 +756,8 @@ class TeleBot:
raise e
else:
time.sleep(error_interval)
logger.info('Stopped polling.')
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
logger.info('Stopped polling.' + warning)
def _exec_task(self, task, *args, **kwargs):
if kwargs and kwargs.get('task_type') == 'handler':
@@ -887,11 +905,11 @@ class TeleBot:
result = apihelper.get_chat_administrators(self.token, chat_id)
return [types.ChatMember.de_json(r) for r in result]
@util.deprecated(deprecation_text="Use get_chat_member_count instead")
def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
"""
This function is deprecated. Use `get_chat_member_count` instead
"""
logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.')
result = apihelper.get_chat_member_count(self.token, chat_id)
return result
@@ -1024,7 +1042,7 @@ class TeleBot:
reply_to_message_id: Optional[int]=None,
allow_sending_without_reply: Optional[bool]=None,
reply_markup: Optional[REPLY_MARKUP_TYPES]=None,
timeout: Optional[int]=None) -> int:
timeout: Optional[int]=None) -> types.MessageID:
"""
Use this method to copy messages of any kind.
@@ -1662,6 +1680,7 @@ class TeleBot:
"""
return apihelper.send_chat_action(self.token, chat_id, action, timeout)
@util.deprecated(deprecation_text="Use ban_chat_member instead")
def kick_chat_member(
self, chat_id: Union[int, str], user_id: int,
until_date:Optional[Union[int, datetime]]=None,
@@ -1669,7 +1688,6 @@ class TeleBot:
"""
This function is deprecated. Use `ban_chat_member` instead
"""
logger.info('kick_chat_member is deprecated. Use ban_chat_member instead.')
return apihelper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages)
def ban_chat_member(
@@ -1772,6 +1790,7 @@ class TeleBot:
can_promote_members: Optional[bool]=None,
is_anonymous: Optional[bool]=None,
can_manage_chat: Optional[bool]=None,
can_manage_video_chats: Optional[bool]=None,
can_manage_voice_chats: Optional[bool]=None) -> bool:
"""
Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator
@@ -1798,15 +1817,22 @@ class TeleBot:
message statistics in channels, see channel members,
see anonymous administrators in supergroups and ignore slow mode.
Implied by any other administrator privilege
:param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats
:param can_manage_video_chats: Bool: Pass True, if the administrator can manage voice chats
For now, bots can use this privilege only for passing to other administrators.
:param can_manage_voice_chats: Deprecated, use can_manage_video_chats.
:return: True on success.
"""
if can_manage_voice_chats is not None:
logger.warning("promote_chat_member: can_manage_voice_chats parameter is deprecated. Use can_manage_video_chats instead.")
if can_manage_video_chats is None:
can_manage_video_chats = can_manage_voice_chats
return apihelper.promote_chat_member(
self.token, chat_id, user_id, can_change_info, can_post_messages,
can_edit_messages, can_delete_messages, can_invite_users,
can_restrict_members, can_pin_messages, can_promote_members,
is_anonymous, can_manage_chat, can_manage_voice_chats)
is_anonymous, can_manage_chat, can_manage_video_chats)
def set_chat_administrator_custom_title(
self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool:
@@ -2033,6 +2059,71 @@ class TeleBot:
result = apihelper.get_my_commands(self.token, scope, language_code)
return [types.BotCommand.de_json(cmd) for cmd in result]
def set_chat_menu_button(self, chat_id: Union[int, str]=None,
menu_button: types.MenuButton=None) -> bool:
"""
Use this method to change the bot's menu button in a private chat,
or the default menu button.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setchatmenubutton
:param chat_id: Unique identifier for the target private chat.
If not specified, default bot's menu button will be changed.
:param menu_button: A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault
"""
return apihelper.set_chat_menu_button(self.token, chat_id, menu_button)
def get_chat_menu_button(self, chat_id: Union[int, str]=None) -> types.MenuButton:
"""
Use this method to get the current value of the bot's menu button
in a private chat, or the default menu button.
Returns MenuButton on success.
Telegram Documentation: https://core.telegram.org/bots/api#getchatmenubutton
:param chat_id: Unique identifier for the target private chat.
If not specified, default bot's menu button will be returned.
:return: types.MenuButton
"""
return types.MenuButton.de_json(apihelper.get_chat_menu_button(self.token, chat_id))
def set_my_default_administrator_rights(self, rights: types.ChatAdministratorRights=None,
for_channels: bool=None) -> bool:
"""
Use this method to change the default administrator rights requested by the bot
when it's added as an administrator to groups or channels.
These rights will be suggested to users, but they are are free to modify
the list before adding the bot.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmydefaultadministratorrights
:param rights: A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared.
:param for_channels: Pass True to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed.
"""
return apihelper.set_my_default_administrator_rights(self.token, rights, for_channels)
def get_my_default_administrator_rights(self, for_channels: bool=None) -> types.ChatAdministratorRights:
"""
Use this method to get the current default administrator rights of the bot.
Returns ChatAdministratorRights on success.
Telegram documentation: https://core.telegram.org/bots/api#getmydefaultadministratorrights
:param for_channels: Pass True to get the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be returned.
:return: types.ChatAdministratorRights
"""
return types.ChatAdministratorRights.de_json(apihelper.get_my_default_administrator_rights(self.token, for_channels))
def set_my_commands(self, commands: List[types.BotCommand],
scope: Optional[types.BotCommandScope]=None,
language_code: Optional[str]=None) -> bool:
@@ -2373,6 +2464,69 @@ class TeleBot:
max_tip_amount, suggested_tip_amounts, protect_content)
return types.Message.de_json(result)
def create_invoice_link(self,
title: str, description: str, payload:str, provider_token: str,
currency: str, prices: List[types.LabeledPrice],
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[List[int]]=None,
provider_data: Optional[str]=None,
photo_url: Optional[str]=None,
photo_size: Optional[int]=None,
photo_width: Optional[int]=None,
photo_height: Optional[int]=None,
need_name: Optional[bool]=None,
need_phone_number: Optional[bool]=None,
need_email: Optional[bool]=None,
need_shipping_address: Optional[bool]=None,
send_phone_number_to_provider: Optional[bool]=None,
send_email_to_provider: Optional[bool]=None,
is_flexible: Optional[bool]=None) -> str:
"""
Use this method to create a link for an invoice.
Returns the created invoice link as String on success.
Telegram documentation:
https://core.telegram.org/bots/api#createinvoicelink
:param title: Product name, 1-32 characters
:param description: Product description, 1-255 characters
:param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user,
use for your internal processes.
:param provider_token: Payments provider token, obtained via @Botfather
:param currency: Three-letter ISO 4217 currency code,
see https://core.telegram.org/bots/payments#supported-currencies
:param prices: Price breakdown, a list of components
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest
:param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider.
A detailed description of required fields should be provided by the payment provider.
:param photo_url: URL of the product photo for the invoice. Can be a photo of the goods
:param photo_size: Photo size in bytes
:param photo_width: Photo width
:param photo_height: Photo height
:param need_name: Pass True, if you require the user's full name to complete the order
:param need_phone_number: Pass True, if you require the user's phone number to complete the order
:param need_email: Pass True, if you require the user's email to complete the order
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
:param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
:param send_email_to_provider: Pass True, if user's email address should be sent to provider
:param is_flexible: Pass True, if the final price depends on the shipping method
:return: Created invoice link as String on success.
"""
result = apihelper.create_invoice_link(
self.token, title, description, payload, provider_token,
currency, prices, max_tip_amount, suggested_tip_amounts, provider_data,
photo_url, photo_size, photo_width, photo_height, need_name, need_phone_number,
need_email, need_shipping_address, send_phone_number_to_provider,
send_email_to_provider, is_flexible)
return result
# noinspection PyShadowingBuiltins
# TODO: rewrite this method like in API
def send_poll(
@@ -2421,6 +2575,8 @@ class TeleBot:
if isinstance(question, types.Poll):
raise RuntimeError("The send_poll signature was changed, please see send_poll function details.")
explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode
return types.Message.de_json(
apihelper.send_poll(
self.token, chat_id,
@@ -2684,6 +2840,22 @@ class TeleBot:
"""
return apihelper.delete_sticker_from_set(self.token, sticker)
def answer_web_app_query(self, web_app_query_id: str, result: types.InlineQueryResultBase) -> types.SentWebAppMessage:
"""
Use this method to set the result of an interaction with a Web App and
send a corresponding message on behalf of the user to the chat from which
the query originated.
On success, a SentWebAppMessage object is returned.
Telegram Documentation: https://core.telegram.org/bots/api#answerwebappquery
:param web_app_query_id: Unique identifier for the query to be answered
:param result: A JSON-serialized object describing the message to be sent
:return:
"""
return apihelper.answer_web_app_query(self.token, web_app_query_id, result)
def register_for_reply(
self, message: types.Message, callback: Callable, *args, **kwargs) -> None:
"""
@@ -3798,7 +3970,7 @@ class TeleBot:
"""
Check middleware
:param message:
:param update_type:
:return:
"""
middlewares = None
@@ -3806,7 +3978,7 @@ class TeleBot:
middlewares = [i for i in self.middlewares if update_type in i.update_types]
return middlewares
def _run_middlewares_and_handler(self, message, handlers, middlewares, *args, **kwargs):
def _run_middlewares_and_handler(self, message, handlers, middlewares):
"""
This class is made to run handler and middleware in queue.
@@ -3827,6 +3999,7 @@ class TeleBot:
return
elif isinstance(result, SkipHandler) and skip_handler is False:
skip_handler = True
try:
if handlers and not skip_handler:
@@ -3838,24 +4011,31 @@ class TeleBot:
params.append(i)
if len(params) == 1:
handler['function'](message)
elif len(params) == 2:
if handler.get('pass_bot') is True:
handler['function'](message, self)
elif handler.get('pass_bot') is False:
handler['function'](message, data)
elif len(params) == 3:
if params[2] == 'bot' and handler.get('pass_bot') is True:
handler['function'](message, data, self)
else:
if "data" in params:
if len(params) == 2:
handler['function'](message, data)
elif len(params) == 3:
handler['function'](message, data=data, bot=self)
else:
logger.error("It is not allowed to pass data and values inside data to the handler. Check your handler: {}".format(handler['function']))
return
elif not handler.get('pass_bot'):
raise RuntimeError('Your handler accepts 3 parameters but pass_bot is False. Please re-check your handler.')
else:
handler['function'](message, self, data)
data_copy = data.copy()
for key in list(data_copy):
# remove data from data_copy if handler does not accept it
if key not in params:
del data_copy[key]
if handler.get('pass_bot'): data_copy["bot"] = self
if len(data_copy) > len(params) - 1: # remove the message parameter
logger.error("You are passing more data than the handler needs. Check your handler: {}".format(handler['function']))
return
handler["function"](message, **data_copy)
except Exception as e:
handler_error = e
@@ -3864,6 +4044,7 @@ class TeleBot:
return self.exception_handler.handle(e)
logging.error(str(e))
return
# remove the bot from data
if middlewares:
for middleware in middlewares:
middleware.post_process(message, data, handler_error)

View File

@@ -47,7 +47,6 @@ CUSTOM_REQUEST_SENDER = None
ENABLE_MIDDLEWARE = False
def _get_req_session(reset=False):
if SESSION_TIME_TO_LIVE:
# If session TTL is set - check time passed
@@ -94,20 +93,14 @@ def _make_request(token, method_name, method='get', params=None, files=None):
if 'timeout' in params:
read_timeout = params.pop('timeout')
connect_timeout = read_timeout
# if 'connect-timeout' in params:
# connect_timeout = params.pop('connect-timeout') + 10
if 'long_polling_timeout' in params:
# For getUpdates: it's the only function with timeout parameter on the BOT API side
# For getUpdates. It's the only function with timeout parameter on the BOT API side
long_polling_timeout = params.pop('long_polling_timeout')
params['timeout'] = long_polling_timeout
# Long polling hangs for a given time. Read timeout should be greater that long_polling_timeout
read_timeout = max(long_polling_timeout + 5, read_timeout)
# Lets stop suppose that user is stupid and assume that he knows what he do...
# read_timeout = read_timeout + 10
# connect_timeout = connect_timeout + 10
params = params or None # Set params to None if empty
result = None
if RETRY_ON_ERROR and RETRY_ENGINE == 1:
got_result = False
@@ -134,6 +127,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
timeout=(connect_timeout, read_timeout), proxies=proxy)
elif RETRY_ON_ERROR and RETRY_ENGINE == 2:
http = _get_req_session()
# noinspection PyUnresolvedReferences
retry_strategy = requests.packages.urllib3.util.retry.Retry(
total=MAX_RETRIES,
)
@@ -274,7 +268,7 @@ def send_message(
def set_webhook(token, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
drop_pending_updates = None, timeout=None):
drop_pending_updates = None, timeout=None, secret_token=None):
method_url = r'setWebhook'
payload = {
'url': url if url else "",
@@ -292,6 +286,8 @@ def set_webhook(token, url=None, certificate=None, max_connections=None, allowed
payload['drop_pending_updates'] = drop_pending_updates
if timeout:
payload['timeout'] = timeout
if secret_token:
payload['secret_token'] = secret_token
return _make_request(token, method_url, params=payload, files=files)
@@ -971,7 +967,7 @@ def promote_chat_member(
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
method_url = 'promoteChatMember'
payload = {'chat_id': chat_id, 'user_id': user_id}
if can_change_info is not None:
@@ -994,8 +990,8 @@ def promote_chat_member(
payload['is_anonymous'] = is_anonymous
if can_manage_chat is not None:
payload['can_manage_chat'] = can_manage_chat
if can_manage_voice_chats is not None:
payload['can_manage_voice_chats'] = can_manage_voice_chats
if can_manage_video_chats is not None:
payload['can_manage_video_chats'] = can_manage_video_chats
return _make_request(token, method_url, params=payload, method='post')
@@ -1139,6 +1135,40 @@ def get_my_commands(token, scope=None, language_code=None):
payload['language_code'] = language_code
return _make_request(token, method_url, params=payload)
def set_chat_menu_button(token, chat_id=None, menu_button=None):
method_url = r'setChatMenuButton'
payload = {}
if chat_id:
payload['chat_id'] = chat_id
if menu_button:
payload['menu_button'] = menu_button.to_json()
return _make_request(token, method_url, params=payload, method='post')
def get_chat_menu_button(token, chat_id=None):
method_url = r'getChatMenuButton'
payload = {}
if chat_id:
payload['chat_id'] = chat_id
return _make_request(token, method_url, params=payload, method='post')
def set_my_default_administrator_rights(token, rights=None, for_channels=None):
method_url = r'setMyDefaultAdministratorRights'
payload = {}
if rights:
payload['rights'] = rights.to_json()
if for_channels is not None:
payload['for_channels'] = for_channels
return _make_request(token, method_url, params=payload, method='post')
def get_my_default_administrator_rights(token, for_channels=None):
method_url = r'getMyDefaultAdministratorRights'
payload = {}
if for_channels:
payload['for_channels'] = for_channels
return _make_request(token, method_url, params=payload, method='post')
def set_my_commands(token, commands, scope=None, language_code=None):
method_url = r'setMyCommands'
@@ -1289,7 +1319,6 @@ def send_game(
payload['allow_sending_without_reply'] = allow_sending_without_reply
if protect_content is not None:
payload['protect_content'] = protect_content
return _make_request(token, method_url, params=payload)
@@ -1536,7 +1565,6 @@ def create_new_sticker_set(
contains_masks=None, mask_position=None, webm_sticker=None):
method_url = 'createNewStickerSet'
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
@@ -1561,7 +1589,6 @@ def create_new_sticker_set(
def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker):
method_url = 'addStickerToSet'
payload = {'user_id': user_id, 'name': name, 'emojis': emojis}
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
@@ -1591,6 +1618,51 @@ def delete_sticker_from_set(token, sticker):
return _make_request(token, method_url, params=payload, method='post')
def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
method_url = 'answerWebAppQuery'
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
return _make_request(token, method_url, params=payload, method='post')
def create_invoice_link(token, title, description, payload, provider_token,
currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None,
photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None,
need_email=None, need_shipping_address=None, send_phone_number_to_provider=None,
send_email_to_provider=None, is_flexible=None):
method_url = r'createInvoiceLink'
payload = {'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token,
'currency': currency, 'prices': _convert_list_json_serializable(prices)}
if max_tip_amount:
payload['max_tip_amount'] = max_tip_amount
if suggested_tip_amounts:
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
if provider_data:
payload['provider_data'] = provider_data
if photo_url:
payload['photo_url'] = photo_url
if photo_size:
payload['photo_size'] = photo_size
if photo_width:
payload['photo_width'] = photo_width
if photo_height:
payload['photo_height'] = photo_height
if need_name is not None:
payload['need_name'] = need_name
if need_phone_number is not None:
payload['need_phone_number'] = need_phone_number
if need_email is not None:
payload['need_email'] = need_email
if need_shipping_address is not None:
payload['need_shipping_address'] = need_shipping_address
if send_phone_number_to_provider is not None:
payload['send_phone_number_to_provider'] = send_phone_number_to_provider
if send_email_to_provider is not None:
payload['send_email_to_provider'] = send_email_to_provider
if is_flexible is not None:
payload['is_flexible'] = is_flexible
return _make_request(token, method_url, params=payload, method='post')
# noinspection PyShadowingBuiltins
def send_poll(
token, chat_id,
@@ -1735,7 +1807,8 @@ class ApiException(Exception):
super(ApiException, self).__init__("A request to the Telegram API was unsuccessful. {0}".format(msg))
self.function_name = function_name
self.result = result
class ApiHTTPException(ApiException):
"""
This class represents an Exception thrown when a call to the
@@ -1747,7 +1820,8 @@ class ApiHTTPException(ApiException):
.format(result.status_code, result.reason, result.text.encode('utf8')),
function_name,
result)
class ApiInvalidJSONException(ApiException):
"""
This class represents an Exception thrown when a call to the
@@ -1759,7 +1833,8 @@ class ApiInvalidJSONException(ApiException):
.format(result.text.encode('utf8')),
function_name,
result)
class ApiTelegramException(ApiException):
"""
This class represents an Exception thrown when a Telegram API returns error code.
@@ -1773,4 +1848,3 @@ class ApiTelegramException(ApiException):
self.result_json = result_json
self.error_code = result_json['error_code']
self.description = result_json['description']

View File

@@ -149,6 +149,7 @@ class AsyncTeleBot:
:return:
"""
if none_stop is not None:
logger.warning("polling: none_stop parameter is deprecated. Use non_stop instead.")
non_stop = none_stop
if skip_pending:
@@ -276,7 +277,7 @@ class AsyncTeleBot:
handler_error = None
data = {}
process_handler = True
params = []
if middlewares:
for middleware in middlewares:
middleware_result = await middleware.pre_process(message, data)
@@ -294,27 +295,34 @@ class AsyncTeleBot:
continue
elif process_update:
try:
params = []
for i in signature(handler['function']).parameters:
params.append(i)
if len(params) == 1:
await handler['function'](message)
break
elif len(params) == 2:
if handler['pass_bot']:
await handler['function'](message, self)
break
else:
await handler['function'](message, data)
break
elif len(params) == 3:
if handler['pass_bot'] and params[1] == 'bot':
await handler['function'](message, self, data)
break
else:
await handler['function'](message, data)
break
else:
if "data" in params:
if len(params) == 2:
await handler['function'](message, data)
elif len(params) == 3:
await handler['function'](message, data=data, bot=self)
else:
logger.error("It is not allowed to pass data and values inside data to the handler. Check your handler: {}".format(handler['function']))
return
else:
data_copy = data.copy()
for key in list(data_copy):
# remove data from data_copy if handler does not accept it
if key not in params:
del data_copy[key]
if handler.get('pass_bot'): data_copy["bot"] = self
if len(data_copy) > len(params) - 1: # remove the message parameter
logger.error("You are passing more data than the handler needs. Check your handler: {}".format(handler['function']))
return
await handler["function"](message, **data_copy)
except Exception as e:
handler_error = e
@@ -1383,7 +1391,7 @@ class AsyncTeleBot:
self.current_states = StatePickleStorage(file_path=filename)
async def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
drop_pending_updates = None, timeout=None):
drop_pending_updates = None, timeout=None, secret_token=None):
"""
Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an
update for the bot, we will send an HTTPS POST request to the specified url,
@@ -1408,10 +1416,11 @@ class AsyncTeleBot:
resolved through DNS
:param drop_pending_updates: Pass True to drop all pending updates
:param timeout: Integer. Request connection timeout
:param secret_token: Secret token to be used to verify the webhook
:return:
"""
return await asyncio_helper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address,
drop_pending_updates, timeout)
drop_pending_updates, timeout, secret_token)
@@ -1500,11 +1509,11 @@ class AsyncTeleBot:
result = await asyncio_helper.get_chat_administrators(self.token, chat_id)
return [types.ChatMember.de_json(r) for r in result]
@util.deprecated(deprecation_text="Use get_chat_member_count instead")
async def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
"""
This function is deprecated. Use `get_chat_member_count` instead
"""
logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.')
result = await asyncio_helper.get_chat_member_count(self.token, chat_id)
return result
@@ -1512,7 +1521,7 @@ class AsyncTeleBot:
"""
Use this method to get the number of members in a chat. Returns Int on success.
Telegram documentation: https://core.telegram.org/bots/api#getchatmemberscount
Telegram documentation: https://core.telegram.org/bots/api#getchatmembercount
:param chat_id:
:return:
@@ -1550,6 +1559,22 @@ class AsyncTeleBot:
result = await asyncio_helper.delete_chat_sticker_set(self.token, chat_id)
return result
async def answer_web_app_query(self, web_app_query_id: str, result: types.InlineQueryResultBase) -> types.SentWebAppMessage:
"""
Use this method to set the result of an interaction with a Web App and
send a corresponding message on behalf of the user to the chat from which
the query originated.
On success, a SentWebAppMessage object is returned.
Telegram Documentation: https://core.telegram.org/bots/api#answerwebappquery
:param web_app_query_id: Unique identifier for the query to be answered
:param result: A JSON-serialized object describing the message to be sent
:return:
"""
return await asyncio_helper.answer_web_app_query(self.token, web_app_query_id, result)
async def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> types.ChatMember:
"""
Use this method to get information about a member of a chat. Returns a ChatMember object on success.
@@ -1563,8 +1588,6 @@ class AsyncTeleBot:
result = await asyncio_helper.get_chat_member(self.token, chat_id, user_id)
return types.ChatMember.de_json(result)
async def send_message(
self, chat_id: Union[int, str], text: str,
parse_mode: Optional[str]=None,
@@ -1639,7 +1662,7 @@ class AsyncTeleBot:
reply_to_message_id: Optional[int]=None,
allow_sending_without_reply: Optional[bool]=None,
reply_markup: Optional[REPLY_MARKUP_TYPES]=None,
timeout: Optional[int]=None) -> int:
timeout: Optional[int]=None) -> types.MessageID:
"""
Use this method to copy messages of any kind.
@@ -1659,6 +1682,8 @@ class AsyncTeleBot:
:param protect_content:
:return: API reply.
"""
parse_mode = self.parse_mode if (parse_mode is None) else parse_mode
return types.MessageID.de_json(
await asyncio_helper.copy_message(self.token, chat_id, from_chat_id, message_id, caption, parse_mode, caption_entities,
disable_notification, reply_to_message_id, allow_sending_without_reply, reply_markup,
@@ -1903,7 +1928,9 @@ class AsyncTeleBot:
"""
if data and not(sticker):
# function typo miss compatibility
logger.warning("send_sticker: data parameter is deprecated. Use sticker instead.")
sticker = data
return types.Message.de_json(
await asyncio_helper.send_data(
self.token, chat_id, sticker, 'sticker',
@@ -1954,6 +1981,7 @@ class AsyncTeleBot:
parse_mode = self.parse_mode if (parse_mode is None) else parse_mode
if data and not(video):
# function typo miss compatibility
logger.warning("send_sticker: data parameter is deprecated. Use video instead.")
video = data
return types.Message.de_json(
@@ -2374,6 +2402,7 @@ class AsyncTeleBot:
can_promote_members: Optional[bool]=None,
is_anonymous: Optional[bool]=None,
can_manage_chat: Optional[bool]=None,
can_manage_video_chats: Optional[bool]=None,
can_manage_voice_chats: Optional[bool]=None) -> bool:
"""
Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator
@@ -2400,15 +2429,22 @@ class AsyncTeleBot:
message statistics in channels, see channel members,
see anonymous administrators in supergroups and ignore slow mode.
Implied by any other administrator privilege
:param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats
:param can_manage_video_chats: Bool: Pass True, if the administrator can manage voice chats
For now, bots can use this privilege only for passing to other administrators.
:param can_manage_voice_chats: Deprecated, use can_manage_video_chats
:return: True on success.
"""
if can_manage_voice_chats is not None:
logger.warning("promote_chat_member: can_manage_voice_chats parameter is deprecated. Use can_manage_video_chats instead.")
if can_manage_video_chats is None:
can_manage_video_chats = can_manage_voice_chats
return await asyncio_helper.promote_chat_member(
self.token, chat_id, user_id, can_change_info, can_post_messages,
can_edit_messages, can_delete_messages, can_invite_users,
can_restrict_members, can_pin_messages, can_promote_members,
is_anonymous, can_manage_chat, can_manage_voice_chats)
is_anonymous, can_manage_chat, can_manage_video_chats)
async def set_chat_administrator_custom_title(
self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool:
@@ -2635,6 +2671,70 @@ class AsyncTeleBot:
result = await asyncio_helper.get_my_commands(self.token, scope, language_code)
return [types.BotCommand.de_json(cmd) for cmd in result]
async def set_chat_menu_button(self, chat_id: Union[int, str]=None,
menu_button: types.MenuButton=None) -> bool:
"""
Use this method to change the bot's menu button in a private chat,
or the default menu button.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setchatmenubutton
:param chat_id: Unique identifier for the target private chat.
If not specified, default bot's menu button will be changed.
:param menu_button: A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault
"""
return await asyncio_helper.set_chat_menu_button(self.token, chat_id, menu_button)
async def get_chat_menu_button(self, chat_id: Union[int, str]=None) -> types.MenuButton:
"""
Use this method to get the current value of the bot's menu button
in a private chat, or the default menu button.
Returns MenuButton on success.
Telegram Documentation: https://core.telegram.org/bots/api#getchatmenubutton
:param chat_id: Unique identifier for the target private chat.
If not specified, default bot's menu button will be returned.
:return: types.MenuButton
"""
return types.MenuButton.de_json(await asyncio_helper.get_chat_menu_button(self.token, chat_id))
async def set_my_default_administrator_rights(self, rights: types.ChatAdministratorRights=None,
for_channels: bool=None) -> bool:
"""
Use this method to change the default administrator rights requested by the bot
when it's added as an administrator to groups or channels.
These rights will be suggested to users, but they are are free to modify
the list before adding the bot.
Returns True on success.
Telegram documentation: https://core.telegram.org/bots/api#setmydefaultadministratorrights
:param rights: A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared.
:param for_channels: Pass True to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed.
"""
return await asyncio_helper.set_my_default_administrator_rights(self.token, rights, for_channels)
async def get_my_default_administrator_rights(self, for_channels: bool=None) -> types.ChatAdministratorRights:
"""
Use this method to get the current default administrator rights of the bot.
Returns ChatAdministratorRights on success.
Telegram documentation: https://core.telegram.org/bots/api#getmydefaultadministratorrights
:param for_channels: Pass True to get the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be returned.
:return: types.ChatAdministratorRights
"""
return types.ChatAdministratorRights.de_json(await asyncio_helper.get_my_default_administrator_rights(self.token, for_channels))
async def set_my_commands(self, commands: List[types.BotCommand],
scope: Optional[types.BotCommandScope]=None,
language_code: Optional[str]=None) -> bool:
@@ -2974,6 +3074,67 @@ class AsyncTeleBot:
max_tip_amount, suggested_tip_amounts, protect_content)
return types.Message.de_json(result)
async def create_invoice_link(self,
title: str, description: str, payload:str, provider_token: str,
currency: str, prices: List[types.LabeledPrice],
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[List[int]]=None,
provider_data: Optional[str]=None,
photo_url: Optional[str]=None,
photo_size: Optional[int]=None,
photo_width: Optional[int]=None,
photo_height: Optional[int]=None,
need_name: Optional[bool]=None,
need_phone_number: Optional[bool]=None,
need_email: Optional[bool]=None,
need_shipping_address: Optional[bool]=None,
send_phone_number_to_provider: Optional[bool]=None,
send_email_to_provider: Optional[bool]=None,
is_flexible: Optional[bool]=None) -> str:
"""
Use this method to create a link for an invoice.
Returns the created invoice link as String on success.
Telegram documentation:
https://core.telegram.org/bots/api#createinvoicelink
:param title: Product name, 1-32 characters
:param description: Product description, 1-255 characters
:param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user,
use for your internal processes.
:param provider_token: Payments provider token, obtained via @Botfather
:param currency: Three-letter ISO 4217 currency code,
see https://core.telegram.org/bots/payments#supported-currencies
:param prices: Price breakdown, a list of components
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest
:param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider.
A detailed description of required fields should be provided by the payment provider.
:param photo_url: URL of the product photo for the invoice. Can be a photo of the goods
:param photo_size: Photo size in bytes
:param photo_width: Photo width
:param photo_height: Photo height
:param need_name: Pass True, if you require the user's full name to complete the order
:param need_phone_number: Pass True, if you require the user's phone number to complete the order
:param need_email: Pass True, if you require the user's email to complete the order
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
:param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
:param send_email_to_provider: Pass True, if user's email address should be sent to provider
:param is_flexible: Pass True, if the final price depends on the shipping method
:return: Created invoice link as String on success.
"""
result = await asyncio_helper.create_invoice_link(
self.token, title, description, payload, provider_token,
currency, prices, max_tip_amount, suggested_tip_amounts, provider_data,
photo_url, photo_size, photo_width, photo_height, need_name, need_phone_number,
need_email, need_shipping_address, send_phone_number_to_provider,
send_email_to_provider, is_flexible)
return result
# noinspection PyShadowingBuiltins
async def send_poll(
self, chat_id: Union[int, str], question: str, options: List[str],
@@ -3022,6 +3183,8 @@ class AsyncTeleBot:
if isinstance(question, types.Poll):
raise RuntimeError("The send_poll signature was changed, please see send_poll function details.")
explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode
return types.Message.de_json(
await asyncio_helper.send_poll(
self.token, chat_id,

View File

@@ -10,8 +10,12 @@ class SimpleCustomFilter(ABC):
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
"""
key: str = None
async def check(self, message):
"""
Perform a check.
@@ -26,8 +30,12 @@ class AdvancedCustomFilter(ABC):
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
"""
key: str = None
async def check(self, message, text):
"""
Perform a check.
@@ -39,7 +47,7 @@ class TextFilter:
"""
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
example of usage is in examples/custom_filters/advanced_text_filter.py
example of usage is in examples/asynchronous_telebot/custom_filters/advanced_text_filter.py
"""
def __init__(self,

View File

@@ -29,6 +29,7 @@ class StatesGroup:
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
value.group = cls
class SkipHandler:

View File

@@ -11,7 +11,6 @@ API_URL = 'https://api.telegram.org/bot{0}/{1}'
from datetime import datetime
import telebot
from telebot import util, logger
@@ -20,23 +19,22 @@ session = None
FILE_URL = None
CONNECT_TIMEOUT = 15
READ_TIMEOUT = 30
LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates)
REQUEST_TIMEOUT = 10
REQUEST_TIMEOUT = None
MAX_RETRIES = 3
logger = telebot.logger
REQUEST_LIMIT = 50
class SessionManager:
def __init__(self) -> None:
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT))
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(
limit=REQUEST_LIMIT
))
async def create_session(self):
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT))
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(
limit=REQUEST_LIMIT
))
return self.session
async def get_session(self):
@@ -63,18 +61,19 @@ async def _process_request(token, url, method='get', params=None, files=None, re
while not got_result and current_try<MAX_RETRIES-1:
current_try +=1
try:
async with session.request(method=method, url=API_URL.format(token, url), data=params, timeout=timeout) as resp:
async with session.request(method=method, url=API_URL.format(token, url), data=params, timeout=timeout, proxy=proxy) as resp:
got_result = True
logger.debug("Request: method={0} url={1} params={2} files={3} request_timeout={4} current_try={5}".format(method, url, params, files, request_timeout, current_try).replace(token, token.split(':')[0] + ":{TOKEN}"))
json_result = await _check_result(url, resp)
if json_result:
got_result = True
return json_result['result']
except (ApiTelegramException,ApiInvalidJSONException, ApiHTTPException) as e:
raise e
except aiohttp.ClientError as e:
logger.error('Aiohttp ClientError: {0}'.format(e.__class__.__name__))
except Exception as e:
logger.error(f'Unkown error: {e.__class__.__name__}')
logger.error(f'Unknown error: {e.__class__.__name__}')
if not got_result:
raise RequestTimeout("Request timeout. Request: method={0} url={1} params={2} files={3} request_timeout={4}".format(method, url, params, files, request_timeout, current_try))
@@ -172,7 +171,7 @@ async def download_file(token, file_path):
async def set_webhook(token, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
drop_pending_updates = None, timeout=None):
drop_pending_updates = None, timeout=None, secret_token=None):
method_url = r'setWebhook'
payload = {
'url': url if url else "",
@@ -190,6 +189,8 @@ async def set_webhook(token, url=None, certificate=None, max_connections=None, a
payload['drop_pending_updates'] = drop_pending_updates
if timeout:
payload['timeout'] = timeout
if secret_token:
payload['secret_token'] = secret_token
return await _process_request(token, method_url, params=payload, files=files)
@@ -218,11 +219,11 @@ async def get_updates(token, offset=None, limit=None,
params = {}
if offset:
params['offset'] = offset
elif limit:
if limit:
params['limit'] = limit
elif timeout:
if timeout:
params['timeout'] = timeout
elif allowed_updates:
if allowed_updates:
params['allowed_updates'] = allowed_updates
return await _process_request(token, method_name, params=params, request_timeout=request_timeout)
@@ -357,6 +358,12 @@ async def delete_chat_sticker_set(token, chat_id):
return await _process_request(token, method_url, params=payload)
async def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
method_url = 'answerWebAppQuery'
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
return await _process_request(token, method_url, params=payload, method='post')
async def get_chat_member(token, chat_id, user_id):
method_url = r'getChatMember'
payload = {'chat_id': chat_id, 'user_id': user_id}
@@ -941,7 +948,7 @@ async def promote_chat_member(
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
method_url = 'promoteChatMember'
payload = {'chat_id': chat_id, 'user_id': user_id}
if can_change_info is not None:
@@ -964,8 +971,8 @@ async def promote_chat_member(
payload['is_anonymous'] = is_anonymous
if can_manage_chat is not None:
payload['can_manage_chat'] = can_manage_chat
if can_manage_voice_chats is not None:
payload['can_manage_voice_chats'] = can_manage_voice_chats
if can_manage_video_chats is not None:
payload['can_manage_video_chats'] = can_manage_video_chats
return await _process_request(token, method_url, params=payload, method='post')
@@ -1106,6 +1113,42 @@ async def get_my_commands(token, scope=None, language_code=None):
payload['language_code'] = language_code
return await _process_request(token, method_url, params=payload)
async def set_chat_menu_button(token, chat_id=None, menu_button=None):
method_url = r'setChatMenuButton'
payload = {}
if chat_id:
payload['chat_id'] = chat_id
if menu_button:
payload['menu_button'] = menu_button.to_json()
return await _process_request(token, method_url, params=payload, method='post')
async def get_chat_menu_button(token, chat_id=None):
method_url = r'getChatMenuButton'
payload = {}
if chat_id:
payload['chat_id'] = chat_id
return await _process_request(token, method_url, params=payload, method='post')
async def set_my_default_administrator_rights(token, rights=None, for_channels=None):
method_url = r'setMyDefaultAdministratorRights'
payload = {}
if rights:
payload['rights'] = rights.to_json()
if for_channels is not None:
payload['for_channels'] = for_channels
return await _process_request(token, method_url, params=payload, method='post')
async def get_my_default_administrator_rights(token, for_channels=None):
method_url = r'getMyDefaultAdministratorRights'
payload = {}
if for_channels:
payload['for_channels'] = for_channels
return await _process_request(token, method_url, params=payload, method='post')
async def set_my_commands(token, commands, scope=None, language_code=None):
method_url = r'setMyCommands'
@@ -1501,7 +1544,6 @@ async def create_new_sticker_set(
contains_masks=None, mask_position=None, webm_sticker=None):
method_url = 'createNewStickerSet'
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
@@ -1526,7 +1568,6 @@ async def create_new_sticker_set(
async def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker):
method_url = 'addStickerToSet'
payload = {'user_id': user_id, 'name': name, 'emojis': emojis}
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
@@ -1560,6 +1601,47 @@ async def delete_sticker_from_set(token, sticker):
return await _process_request(token, method_url, params=payload, method='post')
async def create_invoice_link(token, title, description, payload, provider_token,
currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None,
photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None,
need_email=None, need_shipping_address=None, send_phone_number_to_provider=None,
send_email_to_provider=None, is_flexible=None):
method_url = r'createInvoiceLink'
payload = {'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token,
'currency': currency, 'prices': await _convert_list_json_serializable(prices)}
if max_tip_amount:
payload['max_tip_amount'] = max_tip_amount
if suggested_tip_amounts:
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
if provider_data:
payload['provider_data'] = provider_data
if photo_url:
payload['photo_url'] = photo_url
if photo_size:
payload['photo_size'] = photo_size
if photo_width:
payload['photo_width'] = photo_width
if photo_height:
payload['photo_height'] = photo_height
if need_name is not None:
payload['need_name'] = need_name
if need_phone_number is not None:
payload['need_phone_number'] = need_phone_number
if need_email is not None:
payload['need_email'] = need_email
if need_shipping_address is not None:
payload['need_shipping_address'] = need_shipping_address
if send_phone_number_to_provider is not None:
payload['send_phone_number_to_provider'] = send_phone_number_to_provider
if send_email_to_provider is not None:
payload['send_email_to_provider'] = send_email_to_provider
if is_flexible is not None:
payload['is_flexible'] = is_flexible
return await _process_request(token, method_url, params=payload, method='post')
# noinspection PyShadowingBuiltins
async def send_poll(
token, chat_id,

View File

@@ -8,7 +8,7 @@ class StateMemoryStorage(StateStorageBase):
async def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:

View File

@@ -1,7 +1,6 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import os
import pickle
@@ -47,14 +46,16 @@ class StatePickleStorage(StateStorageBase):
file.close()
async def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
self.update_data()
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
self.update_data()
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()

View File

@@ -1,6 +1,7 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import json
redis_installed = True
try:
import aioredis
@@ -34,7 +35,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to get record from database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
result = await self.redis.get(self.prefix+str(key))
if result: return json.loads(result)
@@ -44,7 +45,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to set record to database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
await self.redis.set(self.prefix+str(key), json.dumps(value))
@@ -54,7 +55,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
await self.redis.delete(self.prefix+str(key))
return True
@@ -65,7 +66,7 @@ class StateRedisStorage(StateStorageBase):
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if response:
if user_id in response:

View File

@@ -1,3 +1,30 @@
"""
Copyright (c) 2017-2018 Alex Root Junior
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
This file was added during the pull request. The maintainers overlooked that it was copied
"as is" from another project and they do not consider it as a right way to develop a project.
However, due to backward compatibility we had to leave this file in the project with the above
copyright added, as it is required by the original project license.
"""
import typing

View File

@@ -12,8 +12,12 @@ class SimpleCustomFilter(ABC):
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
"""
key: str = None
def check(self, message):
"""
Perform a check.
@@ -28,8 +32,12 @@ class AdvancedCustomFilter(ABC):
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
"""
key: str = None
def check(self, message, text):
"""
Perform a check.

211
telebot/formatting.py Normal file
View File

@@ -0,0 +1,211 @@
"""
Markdown & HTML formatting functions.
.. versionadded:: 4.5.1
"""
import html
import re
def format_text(*args, separator="\n"):
"""
Formats a list of strings into a single string.
.. code:: python
format_text( # just an example
mbold('Hello'),
mitalic('World')
)
:param args: Strings to format.
:param separator: The separator to use between each string.
"""
return separator.join(args)
def escape_html(content: str) -> str:
"""
Escapes HTML characters in a string of HTML.
:param content: The string of HTML to escape.
"""
return html.escape(content)
def escape_markdown(content: str) -> str:
"""
Escapes Markdown characters in a string of Markdown.
Credits: simonsmh
:param content: The string of Markdown to escape.
"""
parse = re.sub(r"([_*\[\]()~`>\#\+\-=|\.!])", r"\\\1", content)
reparse = re.sub(r"\\\\([_*\[\]()~`>\#\+\-=|\.!])", r"\1", parse)
return reparse
def mbold(content: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted bold string.
:param content: The string to bold.
:param escape: True if you need to escape special characters.
"""
return '*{}*'.format(escape_markdown(content) if escape else content)
def hbold(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted bold string.
:param content: The string to bold.
:param escape: True if you need to escape special characters.
"""
return '<b>{}</b>'.format(escape_html(content) if escape else content)
def mitalic(content: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted italic string.
:param content: The string to italicize.
:param escape: True if you need to escape special characters.
"""
return '_{}_\r'.format(escape_markdown(content) if escape else content)
def hitalic(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted italic string.
:param content: The string to italicize.
:param escape: True if you need to escape special characters.
"""
return '<i>{}</i>'.format(escape_html(content) if escape else content)
def munderline(content: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted underline string.
:param content: The string to underline.
:param escape: True if you need to escape special characters.
"""
return '__{}__'.format(escape_markdown(content) if escape else content)
def hunderline(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted underline string.
:param content: The string to underline.
:param escape: True if you need to escape special characters.
"""
return '<u>{}</u>'.format(escape_html(content) if escape else content)
def mstrikethrough(content: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted strikethrough string.
:param content: The string to strikethrough.
:param escape: True if you need to escape special characters.
"""
return '~{}~'.format(escape_markdown(content) if escape else content)
def hstrikethrough(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted strikethrough string.
:param content: The string to strikethrough.
:param escape: True if you need to escape special characters.
"""
return '<s>{}</s>'.format(escape_html(content) if escape else content)
def mspoiler(content: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted spoiler string.
:param content: The string to spoiler.
:param escape: True if you need to escape special characters.
"""
return '||{}||'.format(escape_markdown(content) if escape else content)
def hspoiler(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted spoiler string.
:param content: The string to spoiler.
:param escape: True if you need to escape special characters.
"""
return '<tg-spoiler>{}</tg-spoiler>'.format(escape_html(content) if escape else content)
def mlink(content: str, url: str, escape: bool=True) -> str:
"""
Returns a Markdown-formatted link string.
:param content: The string to link.
:param url: The URL to link to.
:param escape: True if you need to escape special characters.
"""
return '[{}]({})'.format(escape_markdown(content), escape_markdown(url) if escape else content)
def hlink(content: str, url: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted link string.
:param content: The string to link.
:param url: The URL to link to.
:param escape: True if you need to escape special characters.
"""
return '<a href="{}">{}</a>'.format(escape_html(url), escape_html(content) if escape else content)
def mcode(content: str, language: str="", escape: bool=True) -> str:
"""
Returns a Markdown-formatted code string.
:param content: The string to code.
:param escape: True if you need to escape special characters.
"""
return '```{}\n{}```'.format(language, escape_markdown(content) if escape else content)
def hcode(content: str, escape: bool=True) -> str:
"""
Returns an HTML-formatted code string.
:param content: The string to code.
:param escape: True if you need to escape special characters.
"""
return '<code>{}</code>'.format(escape_html(content) if escape else content)
def hpre(content: str, escape: bool=True, language: str="") -> str:
"""
Returns an HTML-formatted preformatted string.
:param content: The string to preformatted.
:param escape: True if you need to escape special characters.
"""
return '<pre><code class="{}">{}</code></pre>'.format(language, escape_html(content) if escape else content)
def hide_link(url: str) -> str:
"""
Hide url of an image.
:param url:
:return: str
"""
return f'<a href="{url}">&#8288;</a>'

View File

@@ -163,6 +163,10 @@ class StatesGroup:
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
value.group = cls
class BaseMiddleware:

View File

@@ -1,5 +1,6 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
class StateMemoryStorage(StateStorageBase):
def __init__(self) -> None:
self.data = {}
@@ -8,7 +9,7 @@ class StateMemoryStorage(StateStorageBase):
def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:

View File

@@ -1,7 +1,6 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
import os
import pickle
@@ -53,14 +52,16 @@ class StatePickleStorage(StateStorageBase):
file.close()
def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
self.update_data()
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
self.update_data()
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()

View File

@@ -1,4 +1,3 @@
from pyclbr import Class
from telebot.storage.base_storage import StateStorageBase, StateContext
import json
@@ -29,7 +28,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to get record from database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
result = connection.get(self.prefix+str(key))
@@ -41,7 +40,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to set record to database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.set(self.prefix+str(key), json.dumps(value))
@@ -52,7 +51,7 @@ class StateRedisStorage(StateStorageBase):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backend compatibility
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.delete(self.prefix+str(key))
@@ -65,7 +64,7 @@ class StateRedisStorage(StateStorageBase):
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if isinstance(state, object):
if hasattr(state, 'name'):
state = state.name
if response:
@@ -177,4 +176,4 @@ class StateRedisStorage(StateStorageBase):
response[user_id]['data'] = dict(data, **response[user_id]['data'])
self.set_record(chat_id, response)
return True

View File

@@ -194,14 +194,15 @@ class WebhookInfo(JsonDeserializable):
return cls(**obj)
def __init__(self, url, has_custom_certificate, pending_update_count, ip_address=None,
last_error_date=None, last_error_message=None, max_connections=None,
allowed_updates=None, **kwargs):
last_error_date=None, last_error_message=None, last_synchronization_error_date=None,
max_connections=None, allowed_updates=None, **kwargs):
self.url = url
self.has_custom_certificate = has_custom_certificate
self.pending_update_count = pending_update_count
self.ip_address = ip_address
self.last_error_date = last_error_date
self.last_error_message = last_error_message
self.last_synchronization_error_date = last_synchronization_error_date
self.max_connections = max_connections
self.allowed_updates = allowed_updates
@@ -214,7 +215,8 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable):
return cls(**obj)
def __init__(self, id, is_bot, first_name, last_name=None, username=None, language_code=None,
can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None, **kwargs):
can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None,
is_premium=None, added_to_attachment_menu=None, **kwargs):
self.id: int = id
self.is_bot: bool = is_bot
self.first_name: str = first_name
@@ -224,6 +226,9 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable):
self.can_join_groups: bool = can_join_groups
self.can_read_all_group_messages: bool = can_read_all_group_messages
self.supports_inline_queries: bool = supports_inline_queries
self.is_premium: bool = is_premium
self.added_to_attachment_menu: bool = added_to_attachment_menu
@property
def full_name(self):
@@ -279,7 +284,8 @@ class Chat(JsonDeserializable):
description=None, invite_link=None, pinned_message=None,
permissions=None, slow_mode_delay=None,
message_auto_delete_time=None, has_protected_content=None, sticker_set_name=None,
can_set_sticker_set=None, linked_chat_id=None, location=None, **kwargs):
can_set_sticker_set=None, linked_chat_id=None, location=None,
join_to_send_messages=None, join_by_request=None, **kwargs):
self.id: int = id
self.type: str = type
self.title: str = title
@@ -288,6 +294,8 @@ class Chat(JsonDeserializable):
self.last_name: str = last_name
self.photo: ChatPhoto = photo
self.bio: str = bio
self.join_to_send_messages: bool = join_to_send_messages
self.join_by_request: bool = join_by_request
self.has_private_forwards: bool = has_private_forwards
self.description: str = description
self.invite_link: str = invite_link
@@ -313,6 +321,20 @@ class MessageID(JsonDeserializable):
self.message_id = message_id
class WebAppData(JsonDeserializable):
def __init__(self, data, button_text):
self.data = data
self.button_text = button_text
def to_dict(self):
return {'data': self.data, 'button_text': self.button_text}
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
obj = cls.check_json(json_string)
return cls(**obj)
class Message(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
@@ -457,18 +479,25 @@ class Message(JsonDeserializable):
opts['proximity_alert_triggered'] = ProximityAlertTriggered.de_json(obj[
'proximity_alert_triggered'])
content_type = 'proximity_alert_triggered'
if 'voice_chat_scheduled' in obj:
opts['voice_chat_scheduled'] = VoiceChatScheduled.de_json(obj['voice_chat_scheduled'])
content_type = 'voice_chat_scheduled'
if 'voice_chat_started' in obj:
opts['voice_chat_started'] = VoiceChatStarted.de_json(obj['voice_chat_started'])
content_type = 'voice_chat_started'
if 'voice_chat_ended' in obj:
opts['voice_chat_ended'] = VoiceChatEnded.de_json(obj['voice_chat_ended'])
content_type = 'voice_chat_ended'
if 'voice_chat_participants_invited' in obj:
opts['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json(obj['voice_chat_participants_invited'])
content_type = 'voice_chat_participants_invited'
if 'video_chat_scheduled' in obj:
opts['video_chat_scheduled'] = VideoChatScheduled.de_json(obj['video_chat_scheduled'])
opts['voice_chat_scheduled'] = opts['video_chat_scheduled'] # deprecated, for backward compatibility
content_type = 'video_chat_scheduled'
if 'video_chat_started' in obj:
opts['video_chat_started'] = VideoChatStarted.de_json(obj['video_chat_started'])
opts['voice_chat_started'] = opts['video_chat_started'] # deprecated, for backward compatibility
content_type = 'video_chat_started'
if 'video_chat_ended' in obj:
opts['video_chat_ended'] = VideoChatEnded.de_json(obj['video_chat_ended'])
opts['voice_chat_ended'] = opts['video_chat_ended'] # deprecated, for backward compatibility
content_type = 'video_chat_ended'
if 'video_chat_participants_invited' in obj:
opts['video_chat_participants_invited'] = VideoChatParticipantsInvited.de_json(obj['video_chat_participants_invited'])
opts['voice_chat_participants_invited'] = opts['video_chat_participants_invited'] # deprecated, for backward compatibility
content_type = 'video_chat_participants_invited'
if 'web_app_data' in obj:
opts['web_app_data'] = WebAppData.de_json(obj['web_app_data'])
content_type = 'web_app_data'
if 'message_auto_delete_timer_changed' in obj:
opts['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json(obj['message_auto_delete_timer_changed'])
content_type = 'message_auto_delete_timer_changed'
@@ -887,7 +916,7 @@ class File(JsonDeserializable):
obj = cls.check_json(json_string, dict_copy=False)
return cls(**obj)
def __init__(self, file_id, file_unique_id, file_size, file_path, **kwargs):
def __init__(self, file_id, file_unique_id, file_size=None, file_path=None, **kwargs):
self.file_id: str = file_id
self.file_unique_id: str = file_unique_id
self.file_size: int = file_size
@@ -919,6 +948,20 @@ class ReplyKeyboardRemove(JsonSerializable):
return json.dumps(json_dict)
class WebAppInfo(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self, url, **kwargs):
self.url: str = url
def to_dict(self):
return {'url': self.url}
class ReplyKeyboardMarkup(JsonSerializable):
max_row_keys = 12
@@ -1011,11 +1054,13 @@ class KeyboardButtonPollType(Dictionaryable):
class KeyboardButton(Dictionaryable, JsonSerializable):
def __init__(self, text: str, request_contact: Optional[bool]=None,
request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None):
request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None,
web_app: WebAppInfo=None):
self.text: str = text
self.request_contact: bool = request_contact
self.request_location: bool = request_location
self.request_poll: KeyboardButtonPollType = request_poll
self.web_app: WebAppInfo = web_app
def to_json(self):
return json.dumps(self.to_dict())
@@ -1028,6 +1073,8 @@ class KeyboardButton(Dictionaryable, JsonSerializable):
json_dict['request_location'] = self.request_location
if self.request_poll is not None:
json_dict['request_poll'] = self.request_poll.to_dict()
if self.web_app is not None:
json_dict['web_app'] = self.web_app.to_dict()
return json_dict
@@ -1122,13 +1169,17 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
obj = cls.check_json(json_string)
if 'login_url' in obj:
obj['login_url'] = LoginUrl.de_json(obj.get('login_url'))
if 'web_app' in obj:
obj['web_app'] = WebAppInfo.de_json(obj.get('web_app'))
return cls(**obj)
def __init__(self, text, url=None, callback_data=None, switch_inline_query=None,
def __init__(self, text, url=None, callback_data=None, web_app=None, switch_inline_query=None,
switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **kwargs):
self.text: str = text
self.url: str = url
self.callback_data: str = callback_data
self.web_app: WebAppInfo = web_app
self.switch_inline_query: str = switch_inline_query
self.switch_inline_query_current_chat: str = switch_inline_query_current_chat
self.callback_game = callback_game # Not Implemented
@@ -1144,6 +1195,8 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
json_dict['url'] = self.url
if self.callback_data:
json_dict['callback_data'] = self.callback_data
if self.web_app:
json_dict['web_app'] = self.web_app.to_dict()
if self.switch_inline_query is not None:
json_dict['switch_inline_query'] = self.switch_inline_query
if self.switch_inline_query_current_chat is not None:
@@ -1235,7 +1288,7 @@ class ChatMember(JsonDeserializable):
can_invite_users=None, can_pin_messages=None, is_member=None,
can_send_messages=None, can_send_media_messages=None, can_send_polls=None,
can_send_other_messages=None, can_add_web_page_previews=None,
can_manage_chat=None, can_manage_voice_chats=None,
can_manage_chat=None, can_manage_video_chats=None,
until_date=None, **kwargs):
self.user: User = user
self.status: str = status
@@ -1257,7 +1310,8 @@ class ChatMember(JsonDeserializable):
self.can_send_other_messages: bool = can_send_other_messages
self.can_add_web_page_previews: bool = can_add_web_page_previews
self.can_manage_chat: bool = can_manage_chat
self.can_manage_voice_chats: bool = can_manage_voice_chats
self.can_manage_video_chats: bool = can_manage_video_chats
self.can_manage_voice_chats: bool = self.can_manage_video_chats # deprecated, for backward compatibility
self.until_date: int = until_date
@@ -1689,6 +1743,26 @@ class InlineQueryResultBase(ABC, Dictionaryable, JsonSerializable):
return json_dict
class SentWebAppMessage(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self, inline_message_id=None):
self.inline_message_id = inline_message_id
def to_dict(self):
json_dict = {}
if self.inline_message_id:
json_dict['inline_message_id'] = self.inline_message_id
return json_dict
class InlineQueryResultArticle(InlineQueryResultBase):
def __init__(self, id, title, input_message_content, reply_markup=None,
url=None, hide_url=None, description=None, thumb_url=None, thumb_width=None, thumb_height=None):
@@ -2509,10 +2583,13 @@ class Sticker(JsonDeserializable):
obj['thumb'] = None
if 'mask_position' in obj:
obj['mask_position'] = MaskPosition.de_json(obj['mask_position'])
if 'premium_animation' in obj:
obj['premium_animation'] = File.de_json(obj['premium_animation'])
return cls(**obj)
def __init__(self, file_id, file_unique_id, width, height, is_animated,
is_video, thumb=None, emoji=None, set_name=None, mask_position=None, file_size=None, **kwargs):
is_video, thumb=None, emoji=None, set_name=None, mask_position=None, file_size=None,
premium_animation=None, **kwargs):
self.file_id: str = file_id
self.file_unique_id: str = file_unique_id
self.width: int = width
@@ -2524,6 +2601,7 @@ class Sticker(JsonDeserializable):
self.set_name: str = set_name
self.mask_position: MaskPosition = mask_position
self.file_size: int = file_size
self.premium_animation: File = premium_animation
@@ -2707,23 +2785,29 @@ class Poll(JsonDeserializable):
obj['explanation_entities'] = Message.parse_entities(obj['explanation_entities'])
return cls(**obj)
# noinspection PyShadowingBuiltins
def __init__(
self,
question, options,
poll_id=None, total_voter_count=None, is_closed=None, is_anonymous=None, poll_type=None,
poll_id=None, total_voter_count=None, is_closed=None, is_anonymous=None, type=None,
allows_multiple_answers=None, correct_option_id=None, explanation=None, explanation_entities=None,
open_period=None, close_date=None, **kwargs):
open_period=None, close_date=None, poll_type=None, **kwargs):
self.id: str = poll_id
self.question: str = question
self.options: List[PollOption] = options
self.total_voter_count: int = total_voter_count
self.is_closed: bool = is_closed
self.is_anonymous: bool = is_anonymous
self.type: str = poll_type
self.type: str = type
if poll_type is not None:
# Wrong param name backward compatibility
logger.warning("Poll: poll_type parameter is deprecated. Use type instead.")
if type is None:
self.type: str = poll_type
self.allows_multiple_answers: bool = allows_multiple_answers
self.correct_option_id: int = correct_option_id
self.explanation: str = explanation
self.explanation_entities: List[MessageEntity] = explanation_entities # Default state of entities is None. if (explanation_entities is not None) else []
self.explanation_entities: List[MessageEntity] = explanation_entities
self.open_period: int = open_period
self.close_date: int = close_date
@@ -2833,7 +2917,7 @@ class ProximityAlertTriggered(JsonDeserializable):
self.distance: int = distance
class VoiceChatStarted(JsonDeserializable):
class VideoChatStarted(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
return cls()
@@ -2845,8 +2929,13 @@ class VoiceChatStarted(JsonDeserializable):
"""
pass
class VoiceChatStarted(VideoChatStarted):
def __init__(self):
logger.warning('VoiceChatStarted is deprecated. Use VideoChatStarted instead.')
super().__init__()
class VoiceChatScheduled(JsonDeserializable):
class VideoChatScheduled(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
@@ -2856,8 +2945,13 @@ class VoiceChatScheduled(JsonDeserializable):
def __init__(self, start_date, **kwargs):
self.start_date: int = start_date
class VoiceChatScheduled(VideoChatScheduled):
def __init__(self, *args, **kwargs):
logger.warning('VoiceChatScheduled is deprecated. Use VideoChatScheduled instead.')
super().__init__(*args, **kwargs)
class VoiceChatEnded(JsonDeserializable):
class VideoChatEnded(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
@@ -2867,8 +2961,14 @@ class VoiceChatEnded(JsonDeserializable):
def __init__(self, duration, **kwargs):
self.duration: int = duration
class VoiceChatEnded(VideoChatEnded):
def __init__(self, *args, **kwargs):
logger.warning('VoiceChatEnded is deprecated. Use VideoChatEnded instead.')
super().__init__(*args, **kwargs)
class VoiceChatParticipantsInvited(JsonDeserializable):
class VideoChatParticipantsInvited(JsonDeserializable):
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
@@ -2880,6 +2980,11 @@ class VoiceChatParticipantsInvited(JsonDeserializable):
def __init__(self, users=None, **kwargs):
self.users: List[User] = users
class VoiceChatParticipantsInvited(VideoChatParticipantsInvited):
def __init__(self, *args, **kwargs):
logger.warning('VoiceChatParticipantsInvited is deprecated. Use VideoChatParticipantsInvited instead.')
super().__init__(*args, **kwargs)
class MessageAutoDeleteTimerChanged(JsonDeserializable):
@classmethod
@@ -2890,3 +2995,114 @@ class MessageAutoDeleteTimerChanged(JsonDeserializable):
def __init__(self, message_auto_delete_time, **kwargs):
self.message_auto_delete_time = message_auto_delete_time
class MenuButton(JsonDeserializable, JsonSerializable):
"""
Base class for MenuButtons.
"""
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
obj = cls.check_json(json_string)
map = {
'commands': MenuButtonCommands,
'web_app': MenuButtonWebApp,
'default': MenuButtonDefault
}
return map[obj['type']](**obj)
def to_json(self):
raise NotImplementedError
class MenuButtonCommands(MenuButton):
def __init__(self, type):
self.type = type
def to_dict(self):
return {'type': self.type}
def to_json(self):
return json.dumps(self.to_dict())
class MenuButtonWebApp(MenuButton):
def __init__(self, type, text, web_app):
self.type: str = type
self.text: str = text
self.web_app: WebAppInfo = web_app
def to_dict(self):
return {'type': self.type, 'text': self.text, 'web_app': self.web_app.to_dict()}
def to_json(self):
return json.dumps(self.to_dict())
class MenuButtonDefault(MenuButton):
def __init__(self, type):
self.type: str = type
def to_dict(self):
return {'type': self.type}
def to_json(self):
return json.dumps(self.to_dict())
class ChatAdministratorRights(JsonDeserializable, JsonSerializable):
"""
Class representation of:
https://core.telegram.org/bots/api#chatadministratorrights
"""
@classmethod
def de_json(cls, json_string):
if json_string is None: return None
obj = cls.check_json(json_string)
return cls(**obj)
def __init__(self, is_anonymous: bool, can_manage_chat: bool,
can_delete_messages: bool, can_manage_video_chats: bool, can_restrict_members: bool,
can_promote_members: bool, can_change_info: bool, can_invite_users: bool,
can_post_messages: bool=None, can_edit_messages: bool=None,
can_pin_messages: bool=None) -> None:
self.is_anonymous: bool = is_anonymous
self.can_manage_chat: bool = can_manage_chat
self.can_delete_messages: bool = can_delete_messages
self.can_manage_video_chats: bool = can_manage_video_chats
self.can_restrict_members: bool = can_restrict_members
self.can_promote_members: bool = can_promote_members
self.can_change_info: bool = can_change_info
self.can_invite_users: bool = can_invite_users
self.can_post_messages: bool = can_post_messages
self.can_edit_messages: bool = can_edit_messages
self.can_pin_messages: bool = can_pin_messages
def to_dict(self):
json_dict = {
'is_anonymous': self.is_anonymous,
'can_manage_chat': self.can_manage_chat,
'can_delete_messages': self.can_delete_messages,
'can_manage_video_chats': self.can_manage_video_chats,
'can_restrict_members': self.can_restrict_members,
'can_promote_members': self.can_promote_members,
'can_change_info': self.can_change_info,
'can_invite_users': self.can_invite_users,
}
if self.can_post_messages is not None:
json_dict['can_post_messages'] = self.can_post_messages
if self.can_edit_messages is not None:
json_dict['can_edit_messages'] = self.can_edit_messages
if self.can_pin_messages is not None:
json_dict['can_pin_messages'] = self.can_pin_messages
return json_dict
def to_json(self):
return json.dumps(self.to_dict())

View File

@@ -5,6 +5,10 @@ import string
import threading
import traceback
from typing import Any, Callable, List, Dict, Optional, Union
import hmac
import json
from hashlib import sha256
from urllib.parse import parse_qsl
# noinspection PyPep8Naming
import queue as Queue
@@ -40,8 +44,8 @@ content_type_media = [
content_type_service = [
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
'video_chat_participants_invited', 'message_auto_delete_timer_changed'
]
update_types = [
@@ -370,7 +374,8 @@ def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.I
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None
'login_url': None,
'web_app': None
}
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
@@ -449,17 +454,22 @@ def generate_random_token():
return ''.join(random.sample(string.ascii_letters, 16))
def deprecated(warn: bool=True, alternative: Optional[Callable]=None):
def deprecated(warn: bool=True, alternative: Optional[Callable]=None, deprecation_text=None):
"""
Use this decorator to mark functions as deprecated.
When the function is used, an info (or warning if `warn` is True) is logged.
:param warn: If True a warning is logged else an info
:param alternative: The new function to use instead
:param deprecation_text: Custom deprecation text
"""
def decorator(function):
def wrapper(*args, **kwargs):
info = f"`{function.__name__}` is deprecated." + (f" Use `{alternative.__name__}` instead" if alternative else "")
info = f"`{function.__name__}` is deprecated."
if alternative:
info += f" Use `{alternative.__name__}` instead"
if deprecation_text:
info += " " + deprecation_text
if not warn:
logger.info(info)
else:
@@ -512,3 +522,36 @@ def antiflood(function, *args, **kwargs):
msg = function(*args, **kwargs)
finally:
return msg
def parse_web_app_data(token: str, raw_init_data: str):
is_valid = validate_WebApp_data(token, raw_init_data)
if not is_valid:
return False
result = {}
for key, value in parse_qsl(raw_init_data):
try:
value = json.loads(value)
except json.JSONDecodeError:
result[key] = value
else:
result[key] = value
return result
def validate_web_app_data(token, raw_init_data):
try:
parsed_data = dict(parse_qsl(raw_init_data))
except ValueError:
return False
if "hash" not in parsed_data:
return False
init_data_hash = parsed_data.pop('hash')
data_check_string = "\n".join(f"{key}={value}" for key, value in sorted(parsed_data.items()))
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=sha256)
return hmac.new(secret_key.digest(), data_check_string.encode(), sha256).hexdigest() == init_data_hash

View File

@@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '4.4.1'
__version__ = '4.6.0'

View File

@@ -6,10 +6,13 @@ from telebot import types
def test_json_user():
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true}'
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true, "is_premium":true, "added_to_attachment_menu": true}'
u = types.User.de_json(jsonstring)
assert u.id == 101176298
assert u.full_name == 'RDSSBOT )))'
assert u.is_premium is True
assert u.added_to_attachment_menu is True
def test_json_message():
@@ -155,12 +158,14 @@ def test_json_update():
def test_json_chat():
json_string = r'{"id": -111111,"title": "Test Title","type": "group"}'
json_string = r'{"id": -111111,"title": "Test Title","type": "group", "join_to_send_messages": true, "join_by_request": true}'
chat = types.Chat.de_json(json_string)
assert chat.id == -111111
assert chat.type == 'group'
assert chat.title == 'Test Title'
assert chat.join_to_send_messages is True
assert chat.join_by_request is True
def test_InlineQueryResultCachedPhoto():
iq = types.InlineQueryResultCachedPhoto('aaa', 'Fileid')
@@ -245,3 +250,26 @@ def test_chat_member_updated():
assert cm_updated.old_chat_member.status == "member"
assert cm_updated.new_chat_member.status == "administrator"
def test_webhook_info():
json_string = r'{"url": "https://example.com/webhook", "has_custom_certificate": true, "pending_update_count": 1, "last_error_date": 0, "last_error_message": "", "last_synchronization_error_date": 489309, "max_connections": 40, "allowed_updates": ["message"]}'
webhook_info = types.WebhookInfo.de_json(json_string)
print(webhook_info)
assert webhook_info.url == 'https://example.com/webhook'
assert webhook_info.has_custom_certificate is True
assert webhook_info.pending_update_count == 1
assert webhook_info.last_error_date == 0
assert webhook_info.last_error_message == ''
assert webhook_info.max_connections == 40
assert webhook_info.last_synchronization_error_date == 489309
assert webhook_info.allowed_updates == ['message']
def test_sent_web_app_message():
json_string = r'{"inline_message_id": "29430"}'
sent_web_app_message = types.SentWebAppMessage.de_json(json_string)
assert sent_web_app_message.inline_message_id == '29430'