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

Compare commits

...

161 Commits
4.3.1 ... 4.4.1

Author SHA1 Message Date
9417c49d8e Merge pull request #1502 from Badiboy/master
Bump version to 4.4.1
2022-04-16 23:22:30 +03:00
1a40f1da7a Bump version to 4.4.1 2022-04-16 23:21:25 +03:00
5e28f27764 Bump version to 4.4.1 2022-04-16 23:17:19 +03:00
110575ca40 Merge pull request #1500 from coder2020official/master
Road to release(1st part)
2022-04-15 22:49:02 +03:00
22b4e636e2 Road to release(1st part) 2022-04-16 00:13:14 +05:00
1688a466f4 Merge pull request #1492 from nj-vs-vh/master
Issue #1491: missing await inserted
2022-04-01 18:54:37 +03:00
625da4cdd9 missing await inserted 2022-03-27 18:32:05 +04:00
a9e0f5b7b0 Merge pull request #1484 from coder2020official/master
Regular documentation update.
2022-03-21 01:36:15 +03:00
7b1b1a7caa Update PULL_REQUEST_TEMPLATE.md 2022-03-19 14:06:07 +05:00
a6477541c0 Documentation incorrect display is fixed now. 2022-03-19 13:49:36 +05:00
b652a9f6dc Update doc_req.txt 2022-03-19 13:26:09 +05:00
73b2813512 Update doc_req.txt 2022-03-10 20:22:42 +05:00
ace28983b6 Merge pull request #1475 from coder2020official/master
Fix
2022-03-08 22:53:55 +03:00
e82675320c Merge pull request #1474 from WuerfelDev/patch-1
Add TranslateThisVideoBot to the bot list
2022-03-08 22:53:13 +03:00
1e88671799 Merge branch 'eternnoir:master' into master 2022-03-08 12:07:09 +05:00
1cdf9640d7 Fix example on chat_member, fix middleware-exception for async 2022-03-08 12:07:00 +05:00
91badb53e5 Merge pull request #1473 from coder2020official/master
Docstrings of sync telebot updated to fit documentation. Basic middleware classes added.
2022-03-07 20:00:55 +03:00
477d02468d Fixed middlewares 2022-03-07 21:40:39 +05:00
9f3a270fae Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-03-07 21:13:49 +05:00
244b058648 Fix 2022-03-07 21:13:30 +05:00
886c9b9bc0 Update README.md 2022-03-07 20:38:22 +05:00
41025ba97b Update setup_python.yml 2022-03-07 20:26:11 +05:00
4875bb6188 Update requirements.txt 2022-03-07 19:04:12 +05:00
5f91c3d4e6 Added python 3.10 to tests 2022-03-07 18:56:16 +05:00
7b62915a5b Create PULL_REQUEST_TEMPLATE.md 2022-03-07 18:54:59 +05:00
05c3cb2c1d Update doc_req.txt 2022-03-07 18:08:41 +05:00
60a96d1400 Update README.md 2022-03-07 17:42:47 +05:00
dcd0df93da Update README.md 2022-03-07 17:39:42 +05:00
f15101fc6f Update doc_req.txt 2022-03-07 17:32:42 +05:00
5f03253398 Fix comments 2022-03-07 17:31:02 +05:00
8fab55e937 Create antiflood_middleware.py 2022-03-07 17:30:10 +05:00
60a23665cb Update flooding_middleware.py 2022-03-07 17:23:55 +05:00
b292b275cb Create basic_example.py 2022-03-07 17:21:59 +05:00
403028bf35 Update README.md 2022-03-07 17:14:51 +05:00
3dda5cff06 Create README.md 2022-03-07 17:14:25 +05:00
dd589e2490 Updated documentation to another theme. 2022-03-07 16:10:44 +05:00
c8fb83c97c Fix documentation 2022-03-07 14:24:28 +05:00
854f6a9506 Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-03-07 13:30:41 +05:00
be0557c2b5 Multiple middlewares allowed for async 2022-03-07 13:30:39 +05:00
436422e4da TranslateThisVideo Bot 2022-03-06 22:29:57 +01:00
fc72576aaa Update README.md 2022-03-07 00:39:25 +05:00
f69a2ba044 Update docstrings for asynctelebot. 2022-03-07 00:18:11 +05:00
c45e06c694 Updated description for TeleBot class 2022-03-06 23:23:33 +05:00
78bdf1ca4e Update docstrings 2022-03-06 23:14:07 +05:00
3c7d3c0196 Fix tests 2022-03-06 19:52:42 +05:00
441a5793cc Update docstrings to correct documentation. 2022-03-06 19:41:54 +05:00
388477686b Added middlewares.
Bumped middlewares
2022-03-06 18:39:41 +05:00
4f654d9e12 Add TranslateThisBot to the bot list 2022-03-06 04:26:40 +01:00
ac12d0fc02 Merge pull request #1467 from coder2020official/master
CallbackQuery usage with states.
2022-03-02 11:56:13 +03:00
b8ebe4fd58 Merge pull request #1469 from abdullaev388/master
argument of 'func' parameter in handlers can be async function
2022-03-01 18:11:20 +03:00
c84896391e argument of 'func' parameter in handlers can be async function 2022-03-01 14:39:00 +05:00
995e28e9d8 Remove unnecessary thing 2022-02-26 22:50:55 +05:00
1bfc082d46 Update documentation 2022-02-26 22:48:03 +05:00
1a35bbb127 Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-02-26 22:43:05 +05:00
e585c77830 Fix 2022-02-26 22:43:03 +05:00
5ca92ff637 Merge pull request #1465 from coder2020official/master
Added isinstance checkups in state filters
2022-02-26 13:07:53 +03:00
f4c76553ed Update asyncio_filters.py 2022-02-25 19:53:17 +05:00
75baf6dd96 Update custom_filters.py 2022-02-25 19:52:56 +05:00
301b9288a4 Merge branch 'eternnoir:master' into master 2022-02-25 19:47:50 +05:00
70b9fc86d2 Update custom_filters.py 2022-02-25 19:46:49 +05:00
dde9cd323c Update asyncio_filters.py 2022-02-25 19:45:52 +05:00
01a6827542 Merge pull request #1462 from coder2020official/master
Allow using non-class states
2022-02-23 11:32:01 +03:00
b960a9e574 Update custom_filters.py 2022-02-23 13:08:02 +05:00
102fe3a8fb Update asyncio_filters.py 2022-02-23 13:07:25 +05:00
292df419ba Merge pull request #1456 from abdullaev388/master
I18N class for sync telebot and middleware for async
2022-02-22 22:37:13 +03:00
7993e1d1c9 corrected setup middleware in async i18n middleware example 2022-02-21 20:08:03 +05:00
7309f92c36 Merge pull request #1459 from coder2020official/master
Fixed documentation and added link
2022-02-19 22:48:37 +03:00
7875ff293d Update README.md 2022-02-20 00:44:08 +05:00
4adac4d852 Update quick_start.rst 2022-02-20 00:40:25 +05:00
38bff65caf removed unused imports from util.py 2022-02-20 00:28:27 +05:00
9ecadf1bc1 Merge pull request #1458 from coder2020official/master
Bump documentation
2022-02-19 22:19:00 +03:00
5d7ae385ec token removed. 2022-02-20 00:12:14 +05:00
74e9780b30 BaseMiddleware returned to it's original place && I18N middleware is now only in examples 2022-02-20 00:08:14 +05:00
9b20f41ece I18N class removed from telebot.util.py 2022-02-19 23:57:21 +05:00
967309120e Merge pull request #1457 from Badiboy/master
Fix check of the regexp and commands types
2022-02-19 21:41:55 +03:00
94be2abdbd Typo 2022-02-19 21:39:52 +03:00
6c31b53cd9 Fix check of the regexp and commands types 2022-02-19 21:39:02 +03:00
9bfc0b2c6f preventet breaking change 2022-02-19 23:37:03 +05:00
fc374ec57a Merge pull request #1454 from Troshchk/message_handler_checking
Additional check of the regexp and commands types
2022-02-19 21:33:30 +03:00
7a8e60ddc2 Update index.rst 2022-02-19 22:41:09 +05:00
7f43f26886 Add telebot 2022-02-19 22:21:15 +05:00
4521982837 Create .readthedocs.yml 2022-02-19 22:17:29 +05:00
30c43b557c Documentation Bump 2022-02-19 21:56:51 +05:00
10b5886dcc Completed I18N examples descriptions 2022-02-19 18:56:27 +05:00
93b97fc3fe I18N middleware example was added 2022-02-19 18:47:36 +05:00
1f6e60fd74 I18N middleware implementation was added 2022-02-19 16:25:46 +05:00
5337d4838d asyncio_middlewares.py was created && BaseMiddleware class was replaced to asyncio_middlewares.py 2022-02-19 16:02:14 +05:00
ae5d183db0 slight TextFilter class improvement 2022-02-19 15:53:58 +05:00
0d85a34551 an example for i18n class was added 2022-02-19 15:07:46 +05:00
002c608d45 i18n class was added 2022-02-19 15:04:31 +05:00
ec766a3e43 Wrapping checking in private methods; warnings changed to errors 2022-02-16 14:05:54 +01:00
0ef8d04ed2 Merge pull request #1449 from abdullaev388/master
new advanced TextFilter was added && An example demostrating TextFilt…
2022-02-16 12:48:29 +03:00
3a86916e72 example of TextFilter starts_with and ends_with usage simultaneously 2022-02-16 12:43:23 +05:00
b41435f407 more descriptive exceptions 2022-02-16 12:29:27 +05:00
f689d90815 Merge pull request #1455 from Badiboy/master
Fix timer_bot.py
2022-02-15 19:55:32 +03:00
966f2e7ef7 Fix timer_bot.py 2022-02-15 19:55:12 +03:00
9075430210 Making first condition shorter, no change in functionality 2022-02-15 15:46:02 +01:00
68095ad69a Adding checks for the commands and regexp input types 2022-02-15 15:24:55 +01:00
8c3d1e608c new TextFilter examples were added 2022-02-12 21:53:40 +05:00
6822f18cbb multiple check patterns && multiple startwith, endswith fields 2022-02-12 21:41:10 +05:00
6e4f2e19d6 async text contains filter was fixed 2022-02-12 20:36:10 +05:00
8bbd062d13 text contains filter was fixed 2022-02-12 20:31:02 +05:00
5f7ccc8c9b created async TextFilter 2022-02-12 17:33:29 +05:00
5b1483f646 removed TextFilterKey in example, instead TextMatchFilter was modified 2022-02-12 17:07:59 +05:00
3cd86d0e93 token. again. 2022-02-12 15:31:54 +05:00
a893fbc358 async advanced callback_data example was added 2022-02-12 15:30:04 +05:00
6fd2a38fe9 An asyncio example demostrating TextFilter usage 2022-02-12 15:12:30 +05:00
b89ecb3e5a modified code 2022-02-12 14:32:59 +05:00
2e5590b566 token removed :) 2022-02-12 14:11:16 +05:00
733bb2ebbb new advanced TextFilter was added && An example demostrating TextFilter usage 2022-02-12 13:35:52 +05:00
64a22457e2 Merge pull request #1446 from abdullaev388/master
New CallbackData example
2022-02-10 12:26:12 +03:00
0c8e94d2c6 advanced usage of callbackdata was added 2022-02-10 13:43:19 +05:00
b9436821e0 callbackdata examples were separated into a directory 2022-02-10 13:33:44 +05:00
a8af9120de Merge pull request #1444 from skelly37/patch-1
Update README.md
2022-02-09 09:51:24 +03:00
0655a1f6b6 Update README.md
added my pyfram-telegram-bot
2022-02-09 00:27:18 +00:00
97dbedaa54 Fix > 2022-02-07 00:57:33 +03:00
4028b44d07 Update README.md 2022-02-06 21:02:14 +03:00
661218c7e3 Merge pull request #1434 from coder2020official/master
Fix States
2022-02-02 13:57:08 +03:00
cd4a9add68 Fix States 2022-02-02 14:44:02 +04:00
7d2915c7f9 Merge pull request #1433 from Badiboy/master
Extend custom exception_handler behaviour
2022-02-02 10:40:58 +03:00
ce56a035b5 Extend custom exception_handler behaviour 2022-02-01 23:58:57 +03:00
9fa79aabc0 Merge pull request #1432 from coder2020official/master
Bot API 5.7
2022-02-01 17:31:35 +03:00
62fad9ca3a Fix tests 2022-02-01 18:16:53 +04:00
388f055643 Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-02-01 17:43:52 +04:00
71be20636a Bot API 5.7 2022-02-01 17:43:49 +04:00
3b38d1b46e Bot API 5.7 in readme 2022-02-01 14:51:36 +04:00
1e0c2ea633 Update README.md 2022-02-01 14:50:36 +04:00
4e7652be7a Bot API 5.7 2022-02-01 14:47:42 +04:00
723075d2da Merge pull request #1430 from barbax7/wrong_setup
Installing storage and asyncio_storage
2022-02-01 09:33:22 +03:00
7ba021871a Adding new way to install library 2022-01-31 23:09:18 +01:00
d7cb819502 Excluding tests and examples from packages to install 2022-01-31 22:58:52 +01:00
5ee2aa77c6 storage and asyncio_storage were not installed with previews setup function 2022-01-31 22:53:31 +01:00
80cf5d8d5b Merge pull request #1427 from barbax7/user
The output of get_me() is already an User object
2022-01-30 19:56:19 +03:00
69277400b7 The output of get_me() is already an User object 2022-01-30 17:53:55 +01:00
8d380b4913 Merge pull request #1423 from coder2020official/master
Code template
2022-01-25 14:42:29 +03:00
23d20e0753 Update README.md 2022-01-25 15:21:09 +04:00
6fc7beba57 Update README.md 2022-01-25 15:18:12 +04:00
8d49d22074 Merge pull request #1422 from Badiboy/master
Code base cleanup
2022-01-25 10:27:10 +03:00
6aa97d055f Bump version to 4.4.0 2022-01-25 10:25:53 +03:00
e55938e23a Keep python 3.6 check 2022-01-25 10:24:45 +03:00
4166fb229e Code base cleanup 2022-01-24 22:38:35 +03:00
2e9947277a Merge pull request #1421 from coder2020official/master
RedisStorage, middleware fix, pass_bot parameter and more
2022-01-24 21:25:16 +03:00
c350ea0ced Comment fixes 2022-01-24 21:34:50 +04:00
588b5c4d89 Fix parameter for example 2022-01-24 21:28:56 +04:00
91d0877c61 Fix parameter name to fit 2022-01-24 21:28:10 +04:00
8045ad56ea States Update 2022-01-24 21:24:56 +04:00
124b07ee44 Create __init__.py 2022-01-24 19:08:34 +04:00
195974ddc1 Fix 2022-01-24 18:33:59 +04:00
2b081b42bb Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-01-24 18:31:44 +04:00
321d241483 Delete types.py 2022-01-24 17:23:40 +04:00
ad4ff5835e Merge branch 'eternnoir:master' into master 2022-01-24 17:15:35 +04:00
a3cda2e0ff Updated sync and async. Fixes and new features. 2022-01-24 17:15:04 +04:00
cf2eb1fec7 Merge pull request #1418 from artyl/master
add default None for get_my_commands, examples for bot.set_my_commands
2022-01-21 22:48:11 +03:00
7eb759d1fd remove unused import 2022-01-21 22:25:06 +03:00
a07bf86c30 add default None for get_my_commands parameters scope and language_code sync\async, add examples for bot.set_my_commands 2022-01-21 21:50:33 +03:00
64c4aca3b7 Merge pull request #1417 from artyl/master
Add timer_bot sync and async example
2022-01-21 14:47:59 +03:00
40465643b9 Add timer_bot sync and async example 2022-01-21 12:32:23 +03:00
56fbf491bc Merge pull request #1411 from studentenherz/master
Removed redundant logger configuration in async_telebot
2022-01-12 10:26:35 +03:00
685c071056 Removed redundant logger configuration in async_telebot that made logs repeated twice 2022-01-11 19:24:16 -03:00
fdbc0e6a61 Merge pull request #1410 from barbax7/patch-1
Correct test for antiflood function
2022-01-10 18:36:15 +03:00
7fe8d27686 Correct test for antiflood function 2022-01-10 16:19:21 +01:00
80 changed files with 5210 additions and 1159 deletions

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,15 @@
## Description
Include changes, new features and etc:
## Describe your tests
How did you test your change?
Python version:
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

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.6','3.7','3.8','3.9', 'pypy-3.6', 'pypy-3.7' ] #'pypy-3.8', 'pypy-3.9' NOT SUPPORTED NOW
python-version: [ '3.6','3.7','3.8','3.9', '3.10', 'pypy-3.7', 'pypy-3.8', 'pypy-3.9']
name: ${{ matrix.python-version }} and tests
steps:
- uses: actions/checkout@v2

3
.gitignore vendored
View File

@ -64,3 +64,6 @@ testMain.py
#VS Code
.vscode/
.DS_Store
# documentation
_build/

19
.readthedocs.yml Normal file
View File

@ -0,0 +1,19 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: doc_req.txt

View File

@ -1,16 +1,20 @@
[![PyPi Package Version](https://img.shields.io/pypi/v/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI)
[![Documentation Status](https://readthedocs.org/projects/pytba/badge/?version=latest)](https://pytba.readthedocs.io/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/eternnoir/pyTelegramBotAPI.svg?branch=master)](https://travis-ci.org/eternnoir/pyTelegramBotAPI)
[![PyPi downloads](https://img.shields.io/pypi/dm/pyTelegramBotAPI.svg)](https://pypi.org/project/pyTelegramBotAPI/)
[![PyPi status](https://img.shields.io/pypi/status/pytelegrambotapi.svg?style=flat-square)](https://pypi.python.org/pypi/pytelegrambotapi)
# <p align="center">pyTelegramBotAPI
<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">Supports both sync and async ways.</p>
<p align="center">Both synchronous and asynchronous.</p>
## <p align="center">Supporting Bot API version: <a href="https://core.telegram.org/bots/api#december-30-2021">5.6</a>!
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
## Contents
* [Getting started](#getting-started)
@ -60,6 +64,7 @@
* [The Telegram Chat Group](#the-telegram-chat-group)
* [Telegram Channel](#telegram-channel)
* [More examples](#more-examples)
* [Code Template](#code-template)
* [Bots using this API](#bots-using-this-api)
## Getting started
@ -67,7 +72,7 @@
This API is tested with Python 3.6-3.10 and Pypy 3.
There are two ways to install the library:
* Installation using pip (a Python package manager)*:
* Installation using pip (a Python package manager):
```
$ pip install pyTelegramBotAPI
@ -79,10 +84,17 @@ $ git clone https://github.com/eternnoir/pyTelegramBotAPI.git
$ cd pyTelegramBotAPI
$ python setup.py install
```
or:
```
$ pip install git+https://github.com/eternnoir/pyTelegramBotAPI.git
```
It is generally recommended to use the first option.
**While the API is production-ready, it is still under development and it has regular updates, do not forget to update it regularly by calling `pip install pytelegrambotapi --upgrade`*
*While the API is production-ready, it is still under development and it has regular updates, do not forget to update it regularly by calling*
```
pip install pytelegrambotapi --upgrade
```
## Writing your first bot
@ -344,7 +356,9 @@ def start(message):
assert message.another_text == message.text + ':changed'
```
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)
#### Custom filters
Also, you can use built-in custom filters. Or, you can create your own filter.
@ -685,7 +699,8 @@ Result will be:
## API conformance
* ✔ [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)
* ✔ [Bot API 5.4](https://core.telegram.org/bots/api#november-5-2021)
@ -746,10 +761,10 @@ As you can see here, keywords are await and async.
Asynchronous tasks depend on processor performance. Many asynchronous tasks can run parallelly, while thread tasks will block each other.
### Differences in AsyncTeleBot
AsyncTeleBot has different middlewares. See example on [middlewares](https://github.com/coder2020official/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot/middleware)
AsyncTeleBot is asynchronous. It uses aiohttp instead of requests module.
### Examples
See more examples in our [examples](https://github.com/coder2020official/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot) folder
See more examples in our [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot) folder
## F.A.Q.
@ -758,7 +773,6 @@ See more examples in our [examples](https://github.com/coder2020official/pyTeleg
Telegram Bot API support new type Chat for message.chat.
- Check the ```type``` attribute in ```Chat``` object:
-
```python
if message.chat.type == "private":
# private chat message
@ -794,6 +808,14 @@ Join the [News channel](https://t.me/pyTelegramBotAPI). Here we will post releas
* [Deep Linking](https://github.com/eternnoir/pyTelegramBotAPI/blob/master/examples/deep_linking.py)
* [next_step_handler Example](https://github.com/eternnoir/pyTelegramBotAPI/blob/master/examples/step_example.py)
## Code Template
Template is a ready folder that contains architecture of basic project.
Here are some examples of template:
* [AsyncTeleBot template](https://github.com/coder2020official/asynctelebot_template)
* [TeleBot template](https://github.com/coder2020official/telebot_template)
## Bots using this API
* [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*
@ -843,5 +865,7 @@ Join the [News channel](https://t.me/pyTelegramBotAPI). Here we will post releas
* [GrandQuiz Bot](https://github.com/Carlosma7/TFM-GrandQuiz) by [Carlosma7](https://github.com/Carlosma7). This bot is a trivia game that allows you to play with people from different ages. This project addresses the use of a system through chatbots to carry out a social and intergenerational game as an alternative to traditional game development.
* [Diccionario de la RAE](https://t.me/dleraebot) ([source](https://github.com/studentenherz/dleraebot)) This bot lets you find difinitions of words in Spanish using [RAE's dictionary](https://dle.rae.es/). It features direct message and inline search.
* [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
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**

5
doc_req.txt Normal file
View File

@ -0,0 +1,5 @@
-r requirements.txt
furo
sphinx_copybutton
git+https://github.com/eternnoir/pyTelegramBotAPI.git

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,43 @@
====================
AsyncTeleBot
====================
AsyncTeleBot methods
--------------------
.. automodule:: telebot.async_telebot
:members:
:undoc-members:
:show-inheritance:
Asyncio filters
---------------
.. automodule:: telebot.asyncio_filters
:members:
:undoc-members:
:show-inheritance:
Asynchronous storage for states
-------------------------------
.. automodule:: telebot.asyncio_storage
:members:
:undoc-members:
:show-inheritance:
Asyncio handler backends
------------------------
.. automodule:: telebot.asyncio_handler_backends
:members:
:undoc-members:
:show-inheritance:

13
docs/source/calldata.rst Normal file
View File

@ -0,0 +1,13 @@
=====================
Callback data factory
=====================
callback\_data file
-----------------------------
.. automodule:: telebot.callback_data
:members:
:undoc-members:
:show-inheritance:

70
docs/source/conf.py Normal file
View File

@ -0,0 +1,70 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'pyTelegramBotAPI'
copyright = '2022, coder2020official'
author = 'coder2020official'
# The full version, including alpha/beta/rc tags
release = '4.4.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc',
"sphinx.ext.autosummary",
"sphinx.ext.napoleon",
"sphinx_copybutton",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'furo'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
#html_logo = 'logo.png'
html_theme_options = {
"light_css_variables": {
"color-brand-primary": "#7C4DFF",
"color-brand-content": "#7C4DFF",
},
"light_logo": "logo.png",
"dark_logo": "logo2.png",
}

63
docs/source/index.rst Normal file
View File

@ -0,0 +1,63 @@
.. pyTelegramBotAPI documentation master file, created by
sphinx-quickstart on Fri Feb 18 20:58:37 2022.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyTelegramBotAPI's documentation!
============================================
=======
TeleBot
=======
TeleBot is synchronous and asynchronous implementation of `Telegram Bot API <https://core.telegram.org/bots/api>`_.
Chats
-----
English chat: `Private chat <https://telegram.me/joinchat/Bn4ixj84FIZVkwhk2jag6A>`__
Russian chat: `@pytelegrambotapi_talks_ru <https://t.me/pytelegrambotapi_talks_ru>`__
News: `@pyTelegramBotAPI <https://t.me/pytelegrambotapi>`__
Pypi: `Pypi <https://pypi.org/project/pyTelegramBotAPI/>`__
Source: `Github repository <https://github.com/eternnoir/pyTelegramBotAPI>`__
Some features:
--------------
Easy to learn and use.
Easy to understand.
Both sync and async.
Examples on features.
States
And more...
Content
--------
.. toctree::
install
quick_start
types
sync_version/index
async_version/index
calldata
util
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

41
docs/source/install.rst Normal file
View File

@ -0,0 +1,41 @@
==================
Installation Guide
==================
Using PIP
----------
.. code-block:: bash
$ pip install pyTelegramBotAPI
Using pipenv
------------
.. code-block:: bash
$ pipenv install pyTelegramBotAPI
By cloning repository
---------------------
.. code-block:: bash
$ git clone https://github.com/eternnoir/pyTelegramBotAPI.git
$ cd pyTelegramBotAPI
$ python setup.py install
Directly using pip
------------------
.. code-block:: bash
$ pip install git+https://github.com/eternnoir/pyTelegramBotAPI.git
It is generally recommended to use the first option.
While the API is production-ready, it is still under development and it has regular updates, do not forget to update it regularly by calling:
.. code-block:: bash
$ pip install pytelegrambotapi --upgrade

View File

@ -0,0 +1,16 @@
===========
Quick start
===========
Synchronous TeleBot
-------------------
.. literalinclude:: ../../examples/echo_bot.py
:language: python
Asynchronous TeleBot
--------------------
.. literalinclude:: ../../examples/asynchronous_telebot/echo_bot.py
:language: python

View File

@ -0,0 +1,35 @@
===============
TeleBot version
===============
TeleBot methods
---------------
.. automodule:: telebot
:members:
:undoc-members:
:show-inheritance:
custom_filters file
------------------------------
.. automodule:: telebot.custom_filters
:members:
:undoc-members:
:show-inheritance:
Synchronous storage for states
-------------------------------
.. automodule:: telebot.storage
:members:
:undoc-members:
:show-inheritance:
handler_backends file
--------------------------------
.. automodule:: telebot.handler_backends
:members:
:undoc-members:
:show-inheritance:

10
docs/source/types.rst Normal file
View File

@ -0,0 +1,10 @@
============
Types of API
============
.. automodule:: telebot.types
:members:
:undoc-members:
:show-inheritance:

12
docs/source/util.rst Normal file
View File

@ -0,0 +1,12 @@
============
Utils
============
util file
-------------------
.. automodule:: telebot.util
:members:
:undoc-members:
:show-inheritance:

View File

@ -9,7 +9,7 @@ import telebot
from telebot import types
# Initialize bot with your token
bot = telebot.TeleBot(TOKEN)
bot = telebot.TeleBot('TOKEN')
# The `users` variable is needed to contain chat ids that are either in the search or in the active dialog, like {chat_id, chat_id}
users = {}
@ -47,7 +47,7 @@ def find(message: types.Message):
if message.chat.id not in users:
bot.send_message(message.chat.id, 'Finding...')
if freeid == None:
if freeid is None:
freeid = message.chat.id
else:
# Question:

View File

@ -0,0 +1,26 @@
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import AdvancedCustomFilter
from telebot.callback_data import CallbackData, CallbackDataFilter
calendar_factory = CallbackData("year", "month", prefix="calendar")
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
class CalendarCallbackFilter(AdvancedCustomFilter):
key = 'calendar_config'
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
key = 'calendar_zoom_config'
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
def bind_filters(bot: AsyncTeleBot):
bot.add_custom_filter(CalendarCallbackFilter())
bot.add_custom_filter(CalendarZoomCallbackFilter())

View File

@ -0,0 +1,92 @@
import calendar
from datetime import date, timedelta
from filters import calendar_factory, calendar_zoom
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
EMTPY_FIELD = '1'
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
def generate_calendar_days(year: int, month: int):
keyboard = InlineKeyboardMarkup(row_width=7)
today = date.today()
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=month, day=1).strftime('%b %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=day,
callback_data=EMTPY_FIELD
)
for day in WEEK_DAYS
])
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
week_buttons = []
for day in week:
day_name = ' '
if day == today.day and today.year == year and today.month == month:
day_name = '🔘'
elif day != 0:
day_name = str(day)
week_buttons.append(
InlineKeyboardButton(
text=day_name,
callback_data=EMTPY_FIELD
)
)
keyboard.add(*week_buttons)
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
keyboard.add(
InlineKeyboardButton(
text='Previous month',
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
),
InlineKeyboardButton(
text='Zoom out',
callback_data=calendar_zoom.new(year=year)
),
InlineKeyboardButton(
text='Next month',
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
),
)
return keyboard
def generate_calendar_months(year: int):
keyboard = InlineKeyboardMarkup(row_width=3)
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=1, day=1).strftime('Year %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=month,
callback_data=calendar_factory.new(year=year, month=month_number)
)
for month_number, month in MONTHS
])
keyboard.add(
InlineKeyboardButton(
text='Previous year',
callback_data=calendar_zoom.new(year=year - 1)
),
InlineKeyboardButton(
text='Next year',
callback_data=calendar_zoom.new(year=year + 1)
)
)
return keyboard

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""
This Example will show you an advanced usage of CallbackData.
In this example calendar was implemented
"""
import asyncio
from datetime import date
from filters import calendar_factory, calendar_zoom, bind_filters
from keyboards import generate_calendar_days, generate_calendar_months, EMTPY_FIELD
from telebot import types
from telebot.async_telebot import AsyncTeleBot
API_TOKEN = ''
bot = AsyncTeleBot(API_TOKEN)
@bot.message_handler(commands='start')
async def start_command_handler(message: types.Message):
await bot.send_message(message.chat.id,
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
"\nPress /calendar to see it.")
@bot.message_handler(commands='calendar')
async def calendar_command_handler(message: types.Message):
now = date.today()
await bot.send_message(message.chat.id, 'Calendar',
reply_markup=generate_calendar_days(year=now.year, month=now.month))
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
async def calendar_action_handler(call: types.CallbackQuery):
callback_data: dict = calendar_factory.parse(callback_data=call.data)
year, month = int(callback_data['year']), int(callback_data['month'])
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_days(year=year, month=month))
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
async def calendar_zoom_out_handler(call: types.CallbackQuery):
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
year = int(callback_data.get('year'))
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_months(year=year))
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
async def callback_empty_field_handler(call: types.CallbackQuery):
await bot.answer_callback_query(call.id)
if __name__ == '__main__':
bind_filters(bot)
asyncio.run(bot.infinity_polling())

View File

@ -31,4 +31,4 @@ async def my_chat_m(message: types.ChatMemberUpdated):
async def delall(message: types.Message):
await bot.delete_message(message.chat.id,message.message_id)
import asyncio
asyncio.run(bot.polling())
asyncio.run(bot.polling(allowed_updates=util.update_types))

View File

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""
This Example will show you usage of TextFilter
In this example you will see how to use TextFilter
with (message_handler, callback_query_handler, poll_handler)
"""
import asyncio
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import TextMatchFilter, TextFilter, IsReplyFilter
bot = AsyncTeleBot("")
@bot.message_handler(text=TextFilter(equals='hello'))
async def hello_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
async def hello_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
async def contains_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
async def contains_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
async def starts_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
async def starts_with_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
async def ends_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
async def ends_with_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(equals='/callback'))
async def send_callback(message: types.Message):
keyboard = types.InlineKeyboardMarkup(
keyboard=[
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
]
)
await bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
async def callback_query_handler(call: types.CallbackQuery):
await bot.answer_callback_query(call.id, call.data, show_alert=True)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
async def callback_query_handler_ignore_case(call: types.CallbackQuery):
await bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
@bot.message_handler(text=TextFilter(equals='/poll'))
async def send_poll(message: types.Message):
await bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
await bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
async def poll_question_handler(poll: types.Poll):
print(poll.question)
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
async def poll_question_handler_ignore_case(poll: types.Poll):
print(poll.question + ' ignore case')
# either hi or contains one of (привет, salom)
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
async def multiple_patterns_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
async def multiple_starts_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
async def multiple_ends_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# !ban /ban .ban !бан /бан .бан
@bot.message_handler(is_reply=True,
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
async def ban_command_handler(message: types.Message):
if len(message.text) == 4 and message.chat.type != 'private':
try:
await bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
await bot.reply_to(message.reply_to_message, 'Banned.')
except Exception as err:
print(err.args)
return
if __name__ == '__main__':
bot.add_custom_filter(TextMatchFilter())
bot.add_custom_filter(IsReplyFilter())
asyncio.run(bot.polling())

View File

@ -1,15 +1,27 @@
import telebot
from telebot import asyncio_filters
from telebot.async_telebot import AsyncTeleBot
bot = AsyncTeleBot('TOKEN')
# list of storages, you can use any storage
from telebot.asyncio_storage import StateMemoryStorage
# new feature for states.
from telebot.asyncio_handler_backends import State, StatesGroup
# default state storage is statememorystorage
bot = AsyncTeleBot('TOKEN', state_storage=StateMemoryStorage())
# Just create different statesgroup
class MyStates(StatesGroup):
name = State() # statesgroup should contain states
surname = State()
age = State()
class MyStates:
name = 1
surname = 2
age = 3
# set_state -> sets a new state
# delete_state -> delets state if exists
# get_state -> returns state if exists
@bot.message_handler(commands=['start'])
@ -17,7 +29,7 @@ async def start_ex(message):
"""
Start command. Here we are starting state
"""
await bot.set_state(message.from_user.id, MyStates.name)
await bot.set_state(message.from_user.id, MyStates.name, message.chat.id)
await bot.send_message(message.chat.id, 'Hi, write me a name')
@ -28,39 +40,45 @@ async def any_state(message):
Cancel state
"""
await bot.send_message(message.chat.id, "Your state was cancelled.")
await bot.delete_state(message.from_user.id)
await bot.delete_state(message.from_user.id, message.chat.id)
@bot.message_handler(state=MyStates.name)
async def name_get(message):
"""
State 1. Will process when user's state is 1.
State 1. Will process when user's state is MyStates.name.
"""
await bot.send_message(message.chat.id, f'Now write me a surname')
await bot.set_state(message.from_user.id, MyStates.surname)
async with bot.retrieve_data(message.from_user.id) as data:
await bot.set_state(message.from_user.id, MyStates.surname, message.chat.id)
async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
data['name'] = message.text
@bot.message_handler(state=MyStates.surname)
async def ask_age(message):
"""
State 2. Will process when user's state is 2.
State 2. Will process when user's state is MyStates.surname.
"""
await bot.send_message(message.chat.id, "What is your age?")
await bot.set_state(message.from_user.id, MyStates.age)
async with bot.retrieve_data(message.from_user.id) as data:
await bot.set_state(message.from_user.id, MyStates.age, message.chat.id)
async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
data['surname'] = message.text
# result
@bot.message_handler(state=MyStates.age, is_digit=True)
async def ready_for_answer(message):
async with bot.retrieve_data(message.from_user.id) as data:
"""
State 3. Will process when user's state is MyStates.age.
"""
async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
await 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")
await bot.delete_state(message.from_user.id)
await bot.delete_state(message.from_user.id, message.chat.id)
#incorrect number
@bot.message_handler(state=MyStates.age, is_digit=False)
async def age_incorrect(message):
"""
Will process for wrong input when state is MyState.age
"""
await bot.send_message(message.chat.id, 'Looks like you are submitting a string in the field age. Please enter a number')
# register filters
@ -68,8 +86,6 @@ async def age_incorrect(message):
bot.add_custom_filter(asyncio_filters.StateFilter(bot))
bot.add_custom_filter(asyncio_filters.IsDigitFilter())
# set saving states into file.
bot.enable_saving_states() # you can delete this if you do not need to save states
import asyncio
asyncio.run(bot.polling())

View File

@ -1,9 +1,7 @@
# Just a little example of middleware handlers
import telebot
from telebot.asyncio_handler_backends import BaseMiddleware
from telebot.asyncio_handler_backends import BaseMiddleware, CancelUpdate
from telebot.async_telebot import AsyncTeleBot
from telebot.async_telebot import CancelUpdate
bot = AsyncTeleBot('TOKEN')
@ -37,4 +35,4 @@ async def start(message):
await bot.send_message(message.chat.id, 'Hello!')
import asyncio
asyncio.run(bot.polling())
asyncio.run(bot.polling())

View File

@ -0,0 +1,120 @@
import contextvars
import gettext
import os
from telebot.asyncio_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 = contextvars.ContextVar('language', default=None)
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.get()
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.get()
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)
async 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
async 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.set(await self.get_user_language(obj=message))
async 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,214 @@
"""
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 telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import TextMatchFilter, TextFilter
from i18n_base_midddleware import I18N
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
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']
async 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 = AsyncTeleBot("", state_storage=storage)
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')
async 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)
await bot.send_message(message.from_user.id, text)
@bot.message_handler(commands='lang')
async def change_language_handler(message: types.Message):
await 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']))
async 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
await bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
@bot.message_handler(commands='plural')
async 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)
await bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
async 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)
await bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_))
@bot.message_handler(commands='menu')
async def menu_handler(message: types.Message):
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
await 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"))
async def return_user_id(message: types.Message):
await bot.send_message(message.chat.id, str(message.from_user.id))
@bot.message_handler(text=l_("My user name"))
async def return_user_id(message: types.Message):
username = message.from_user.username
if not username:
username = '-'
await 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))
async def return_user_id(message: types.Message):
await 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))
async def missed_message(message: types.Message):
await 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

@ -0,0 +1,35 @@
#!/usr/bin/python
# This is a set_my_commands example.
# Press on [/] button in telegram client.
# Important, to update the command menu, be sure to exit the chat with the bot and log in again
# Important, command for chat_id and for group have a higher priority than for all
import asyncio
import telebot
from telebot.async_telebot import AsyncTeleBot
API_TOKEN = '<api_token>'
bot = AsyncTeleBot(API_TOKEN)
async def main():
# use in for delete with the necessary scope and language_code if necessary
await bot.delete_my_commands(scope=None, language_code=None)
await bot.set_my_commands(
commands=[
telebot.types.BotCommand("command1", "command1 description"),
telebot.types.BotCommand("command2", "command2 description")
],
# scope=telebot.types.BotCommandScopeChat(12345678) # use for personal command menu for users
# scope=telebot.types.BotCommandScopeAllPrivateChats() # use for all private chats
)
cmd = await bot.get_my_commands(scope=None, language_code=None)
print([c.to_json() for c in cmd])
if __name__ == '__main__':
asyncio.run(main())

View File

@ -0,0 +1,52 @@
#!/usr/bin/python3
# This is a simple bot with schedule timer
# https://github.com/ibrb/python-aioschedule
# https://schedule.readthedocs.io
import asyncio
import aioschedule
from telebot.async_telebot import AsyncTeleBot
API_TOKEN = '<api_token>'
bot = AsyncTeleBot(API_TOKEN)
async def beep(chat_id) -> None:
"""Send the beep message."""
await bot.send_message(chat_id, text='Beep!')
aioschedule.clear(chat_id) # return schedule.CancelJob not working in aioschedule use tag for delete
@bot.message_handler(commands=['help', 'start'])
async def send_welcome(message):
await bot.reply_to(message, "Hi! Use /set <seconds> to set a timer")
@bot.message_handler(commands=['set'])
async def set_timer(message):
args = message.text.split()
if len(args) > 1 and args[1].isdigit():
sec = int(args[1])
aioschedule.every(sec).seconds.do(beep, message.chat.id).tag(message.chat.id)
else:
await bot.reply_to(message, 'Usage: /set <seconds>')
@bot.message_handler(commands=['unset'])
def unset_timer(message):
aioschedule.clean(message.chat.id)
async def scheduler():
while True:
await aioschedule.run_pending()
await asyncio.sleep(1)
async def main():
await asyncio.gather(bot.infinity_polling(), scheduler())
if __name__ == '__main__':
asyncio.run(main())

View File

@ -0,0 +1,25 @@
import telebot
from telebot import types, AdvancedCustomFilter
from telebot.callback_data import CallbackData, CallbackDataFilter
calendar_factory = CallbackData("year", "month", prefix="calendar")
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
class CalendarCallbackFilter(AdvancedCustomFilter):
key = 'calendar_config'
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
key = 'calendar_zoom_config'
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
def bind_filters(bot: telebot.TeleBot):
bot.add_custom_filter(CalendarCallbackFilter())
bot.add_custom_filter(CalendarZoomCallbackFilter())

View File

@ -0,0 +1,92 @@
import calendar
from datetime import date, timedelta
from examples.callback_data_examples.advanced_calendar_example.filters import calendar_factory, calendar_zoom
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
EMTPY_FIELD = '1'
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
def generate_calendar_days(year: int, month: int):
keyboard = InlineKeyboardMarkup(row_width=7)
today = date.today()
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=month, day=1).strftime('%b %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=day,
callback_data=EMTPY_FIELD
)
for day in WEEK_DAYS
])
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
week_buttons = []
for day in week:
day_name = ' '
if day == today.day and today.year == year and today.month == month:
day_name = '🔘'
elif day != 0:
day_name = str(day)
week_buttons.append(
InlineKeyboardButton(
text=day_name,
callback_data=EMTPY_FIELD
)
)
keyboard.add(*week_buttons)
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
keyboard.add(
InlineKeyboardButton(
text='Previous month',
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
),
InlineKeyboardButton(
text='Zoom out',
callback_data=calendar_zoom.new(year=year)
),
InlineKeyboardButton(
text='Next month',
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
),
)
return keyboard
def generate_calendar_months(year: int):
keyboard = InlineKeyboardMarkup(row_width=3)
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=1, day=1).strftime('Year %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=month,
callback_data=calendar_factory.new(year=year, month=month_number)
)
for month_number, month in MONTHS
])
keyboard.add(
InlineKeyboardButton(
text='Previous year',
callback_data=calendar_zoom.new(year=year - 1)
),
InlineKeyboardButton(
text='Next year',
callback_data=calendar_zoom.new(year=year + 1)
)
)
return keyboard

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
This Example will show you an advanced usage of CallbackData.
In this example calendar was implemented
"""
from datetime import date
from examples.callback_data_examples.advanced_calendar_example.keyboards import generate_calendar_days, \
generate_calendar_months, EMTPY_FIELD
from filters import calendar_factory, calendar_zoom, bind_filters
from telebot import types, TeleBot
API_TOKEN = ''
bot = TeleBot(API_TOKEN)
@bot.message_handler(commands='start')
def start_command_handler(message: types.Message):
bot.send_message(message.chat.id,
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
"\nPress /calendar to see it.")
@bot.message_handler(commands='calendar')
def calendar_command_handler(message: types.Message):
now = date.today()
bot.send_message(message.chat.id, 'Calendar', reply_markup=generate_calendar_days(year=now.year, month=now.month))
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
def calendar_action_handler(call: types.CallbackQuery):
callback_data: dict = calendar_factory.parse(callback_data=call.data)
year, month = int(callback_data['year']), int(callback_data['month'])
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_days(year=year, month=month))
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
def calendar_zoom_out_handler(call: types.CallbackQuery):
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
year = int(callback_data.get('year'))
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_months(year=year))
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
def callback_empty_field_handler(call: types.CallbackQuery):
bot.answer_callback_query(call.id)
if __name__ == '__main__':
bind_filters(bot)
bot.infinity_polling()

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
"""
This Example will show you usage of TextFilter
In this example you will see how to use TextFilter
with (message_handler, callback_query_handler, poll_handler)
"""
from telebot import TeleBot, types
from telebot.custom_filters import TextFilter, TextMatchFilter, IsReplyFilter
bot = TeleBot("")
@bot.message_handler(text=TextFilter(equals='hello'))
def hello_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
def hello_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
def contains_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
def contains_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
def starts_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
def starts_with_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
def ends_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
def ends_with_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(equals='/callback'))
def send_callback(message: types.Message):
keyboard = types.InlineKeyboardMarkup(
keyboard=[
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
]
)
bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
def callback_query_handler(call: types.CallbackQuery):
bot.answer_callback_query(call.id, call.data, show_alert=True)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
def callback_query_handler_ignore_case(call: types.CallbackQuery):
bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
@bot.message_handler(text=TextFilter(equals='/poll'))
def send_poll(message: types.Message):
bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
def poll_question_handler(poll: types.Poll):
print(poll.question)
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
def poll_question_handler_ignore_case(poll: types.Poll):
print(poll.question + ' ignore case')
# either hi or contains one of (привет, salom)
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
def multiple_patterns_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
def multiple_starts_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
def multiple_ends_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# !ban /ban .ban !бан /бан .бан
@bot.message_handler(is_reply=True,
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
def ban_command_handler(message: types.Message):
if len(message.text) == 4 and message.chat.type != 'private':
try:
bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
bot.reply_to(message.reply_to_message, 'Banned.')
except Exception as err:
print(err.args)
return
if __name__ == '__main__':
bot.add_custom_filter(TextMatchFilter())
bot.add_custom_filter(IsReplyFilter())
bot.infinity_polling()

View File

@ -1,14 +1,39 @@
import telebot
import telebot # telebot
from telebot import custom_filters
from telebot.handler_backends import State, StatesGroup #States
bot = telebot.TeleBot("")
# States storage
from telebot.storage import StateMemoryStorage
class MyStates:
name = 1
surname = 2
age = 3
# Starting from version 4.4.0+, we support storages.
# StateRedisStorage -> Redis-based storage.
# StatePickleStorage -> Pickle-based storage.
# For redis, you will need to install redis.
# Pass host, db, password, or anything else,
# if you need to change config for redis.
# Pickle requires path. Default path is in folder .state-saves.
# If you were using older version of pytba for pickle,
# you need to migrate from old pickle to new by using
# StatePickleStorage().convert_old_to_new()
# Now, you can pass storage to bot.
state_storage = StateMemoryStorage() # you can init here another storage
bot = telebot.TeleBot("TOKEN",
state_storage=state_storage)
# States group.
class MyStates(StatesGroup):
# Just name variables differently
name = State() # creating instances of State class is enough from now
surname = State()
age = State()
@ -17,50 +42,56 @@ def start_ex(message):
"""
Start command. Here we are starting state
"""
bot.set_state(message.from_user.id, MyStates.name)
bot.set_state(message.from_user.id, MyStates.name, message.chat.id)
bot.send_message(message.chat.id, 'Hi, write me a name')
# Any state
@bot.message_handler(state="*", commands='cancel')
def any_state(message):
"""
Cancel state
"""
bot.send_message(message.chat.id, "Your state was cancelled.")
bot.delete_state(message.from_user.id)
bot.delete_state(message.from_user.id, message.chat.id)
@bot.message_handler(state=MyStates.name)
def name_get(message):
"""
State 1. Will process when user's state is 1.
State 1. Will process when user's state is MyStates.name.
"""
bot.send_message(message.chat.id, f'Now write me a surname')
bot.set_state(message.from_user.id, MyStates.surname)
with bot.retrieve_data(message.from_user.id) as data:
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
@bot.message_handler(state=MyStates.surname)
def ask_age(message):
"""
State 2. Will process when user's state is 2.
State 2. Will process when user's state is MyStates.surname.
"""
bot.send_message(message.chat.id, "What is your age?")
bot.set_state(message.from_user.id, MyStates.age)
with bot.retrieve_data(message.from_user.id) as data:
bot.set_state(message.from_user.id, MyStates.age, message.chat.id)
with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
data['surname'] = message.text
# result
@bot.message_handler(state=MyStates.age, is_digit=True)
def ready_for_answer(message):
with bot.retrieve_data(message.from_user.id) as data:
"""
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")
bot.delete_state(message.from_user.id)
bot.delete_state(message.from_user.id, message.chat.id)
#incorrect number
@bot.message_handler(state=MyStates.age, is_digit=False)
def age_incorrect(message):
"""
Wrong response for MyStates.age
"""
bot.send_message(message.chat.id, 'Looks like you are submitting a string in the field age. Please enter a number')
# register filters
@ -68,7 +99,4 @@ def age_incorrect(message):
bot.add_custom_filter(custom_filters.StateFilter(bot))
bot.add_custom_filter(custom_filters.IsDigitFilter())
# set saving states into file.
bot.enable_saving_states() # you can delete this if you do not need to save states
bot.infinity_polling(skip_pending=True)

View File

@ -0,0 +1,66 @@
import gettext
import os
class I18N:
"""
This class provides high-level tool for internationalization
It is based on gettext util.
"""
def __init__(self, translations_path, domain_name: str):
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 not lang or 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 not lang or lang not in self.translations:
if n == 1:
return singular
return plural
translator = self.translations[lang]
return translator.ngettext(singular, plural, n)
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,23 @@
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
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(_, lang):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
]
]
)

View File

@ -0,0 +1,51 @@
# 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-18 17:54+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 ""
#: main.py:78
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 ""
#: main.py:102
msgid "Language has been changed"
msgstr ""
#: main.py:114
#, fuzzy
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] ""
msgstr[1] ""
#: main.py:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""

View File

@ -0,0 +1,60 @@
# 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-18 17:54+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 "Клик"
#: main.py:78
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 ""
"Привет, {user_fist_name}!\n"
"Это пример мультиязычного бота.\n"
"Доступные команды:\n"
"\n"
"/lang - изменить язык\n"
"/plural - пример плюрализации"
#: main.py:102
msgid "Language has been changed"
msgstr "Язык был сменён"
#: main.py:114
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "У вас {number} клик"
msgstr[1] "У вас {number} клика"
msgstr[2] "У вас {number} кликов"
#: main.py:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Это кликер.\n"
"\n"

View File

@ -0,0 +1,58 @@
# 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-18 17:54+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"
#: main.py:78
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 ""
"Salom, {user_fist_name}!\n"
"Bu multilanguage bot misoli.\n"
"Mavjud buyruqlar:\n"
"\n"
"/lang - tilni ozgartirish\n"
"/plural - pluralizatsiya misoli"
#: main.py:102
msgid "Language has been changed"
msgstr "Til ozgartirildi"
#: main.py:114
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:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Bu clicker.\n"
"\n"

View File

@ -0,0 +1,153 @@
"""
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
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
"""
from functools import wraps
import keyboards
from telebot import TeleBot, types, custom_filters
from telebot.storage.memory_storage import StateMemoryStorage
from i18n_class import I18N
storage = StateMemoryStorage()
bot = TeleBot("", state_storage=storage)
i18n = I18N(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 = {}
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.message_handler(commands='start')
@get_user_language
def start_handler(message: types.Message, lang):
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)
# 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=custom_filters.TextFilter(contains=['en', 'ru', 'uz_Latn']))
def language_handler(call: types.CallbackQuery):
lang = call.data
users_lang[call.from_user.id] = lang
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):
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,
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))
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
@get_user_language
def click_handler(call: types.CallbackQuery, lang):
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,
lang=lang
)
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_, lang))
if __name__ == '__main__':
bot.add_custom_filter(custom_filters.TextMatchFilter())
bot.infinity_polling()

View File

@ -0,0 +1,35 @@
# Middlewares
## Type of middlewares in pyTelegramBotAPI
Currently, synchronous version of pyTelegramBotAPI has two types of middlewares:
- Class-based middlewares
- Function-based middlewares
## Purpose of middlewares
Middlewares are designed to get update before handler's execution.
## Class-based middlewares
This type of middleware has more functionality compared to function-based one.
Class based middleware should be instance of `telebot.handler_backends.BaseMiddleware.`
Each middleware should have 2 main functions:
`pre_process` -> is a method of class which receives update, and data.
Data - is a dictionary, which could be passed right to handler, and `post_process` function.
`post_process` -> is a function of class which receives update, data, and exception, that happened in handler. If handler was executed correctly - then exception will equal to None.
## Function-based middlewares
To use function-based middleware, you should set `apihelper.ENABLE_MIDDLEWARE = True`.
This type of middleware is created by using a decorator for middleware.
With this type middleware, you can retrieve update immediately after update came. You should set update_types as well.
## Why class-based middlewares are better?
- You can pass data between post, pre_process functions, and handler.
- If there is an exception in handler, you will get exception parameter with exception class in post_process.
- Has post_process -> method which works after the handler's execution.
## Take a look at examples for more.

View File

@ -0,0 +1,39 @@
# Just a little example of middleware handlers
from telebot.handler_backends import BaseMiddleware
from telebot import TeleBot
from telebot.handler_backends import CancelUpdate
bot = TeleBot('TOKEN',
use_class_middlewares=True) # if you don't set it to true, middlewares won't work
class SimpleMiddleware(BaseMiddleware):
def __init__(self, limit) -> None:
self.last_time = {}
self.limit = limit
self.update_types = ['message']
# Always specify update types, otherwise middlewares won't work
def pre_process(self, message, data):
if not message.from_user.id in self.last_time:
# User is not in a dict, so lets add and cancel this function
self.last_time[message.from_user.id] = message.date
return
if message.date - self.last_time[message.from_user.id] < self.limit:
# User is flooding
bot.send_message(message.chat.id, 'You are making request too often')
return CancelUpdate()
self.last_time[message.from_user.id] = message.date
def post_process(self, message, data, exception):
pass
bot.setup_middleware(SimpleMiddleware(2))
@bot.message_handler(commands=['start'])
def start(message): # you don't have to put data in handler.
bot.send_message(message.chat.id, 'Hello!')
bot.infinity_polling()

View File

@ -0,0 +1,32 @@
from telebot import TeleBot
from telebot.handler_backends import BaseMiddleware
bot = TeleBot('TOKEN', use_class_middlewares=True) # set use_class_middlewares to True!
# otherwise, class-based middlewares won't execute.
# You can use this classes for cancelling update or skipping handler:
# from telebot.handler_backends import CancelUpdate, SkipHandler
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)
@bot.message_handler(commands=['start'])
def start(message, data: dict): # you don't have to put data parameter in handler if you don't need it.
bot.send_message(message.chat.id, data['foo'])
data['foo'] = 'Processed' # we changed value of data.. this data is now passed to post_process.
# Setup middleware
bot.setup_middleware(Middleware())
bot.infinity_polling()

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# This is a set_my_commands example.
# Press on [/] button in telegram client.
# Important, to update the command menu, be sure to exit the chat with the bot and enter to chat again
# Important, command for chat_id and for group have a higher priority than for all
import telebot
API_TOKEN = '<api_token>'
bot = telebot.TeleBot(API_TOKEN)
# use in for delete with the necessary scope and language_code if necessary
bot.delete_my_commands(scope=None, language_code=None)
bot.set_my_commands(
commands=[
telebot.types.BotCommand("command1", "command1 description"),
telebot.types.BotCommand("command2", "command2 description")
],
# scope=telebot.types.BotCommandScopeChat(12345678) # use for personal command for users
# scope=telebot.types.BotCommandScopeAllPrivateChats() # use for all private chats
)
# check command
cmd = bot.get_my_commands(scope=None, language_code=None)
print([c.to_json() for c in cmd])

42
examples/timer_bot.py Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/python
# This is a simple bot with schedule timer
# https://schedule.readthedocs.io
import time, threading, schedule
from telebot import TeleBot
API_TOKEN = '<api_token>'
bot = TeleBot(API_TOKEN)
@bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
bot.reply_to(message, "Hi! Use /set <seconds> to set a timer")
def beep(chat_id) -> None:
"""Send the beep message."""
bot.send_message(chat_id, text='Beep!')
@bot.message_handler(commands=['set'])
def set_timer(message):
args = message.text.split()
if len(args) > 1 and args[1].isdigit():
sec = int(args[1])
schedule.every(sec).seconds.do(beep, message.chat.id).tag(message.chat.id)
else:
bot.reply_to(message, 'Usage: /set <seconds>')
@bot.message_handler(commands=['unset'])
def unset_timer(message):
schedule.clear(message.chat.id)
if __name__ == '__main__':
threading.Thread(target=bot.infinity_polling, name='bot_infinity_polling', daemon=True).start()
while True:
schedule.run_pending()
time.sleep(1)

View File

@ -5,6 +5,7 @@
# Documenation to Tornado: http://tornadoweb.org
import signal
from typing import Optional, Awaitable
import tornado.httpserver
import tornado.ioloop
@ -33,12 +34,18 @@ bot = telebot.TeleBot(API_TOKEN)
class Root(tornado.web.RequestHandler):
def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
pass
def get(self):
self.write("Hi! This is webhook example!")
self.finish()
class webhook_serv(tornado.web.RequestHandler):
class WebhookServ(tornado.web.RequestHandler):
def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
pass
def get(self):
self.write("What are you doing here?")
self.finish()
@ -93,7 +100,7 @@ tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
application = tornado.web.Application([
(r"/", Root),
(r"/" + WEBHOOK_SECRET, webhook_serv)
(r"/" + WEBHOOK_SECRET, WebhookServ)
])
http_server = tornado.httpserver.HTTPServer(application, ssl_options={

View File

@ -1,4 +1,4 @@
pytest==3.0.2
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

@ -1,5 +1,5 @@
#!/usr/bin/env python
from setuptools import setup
from setuptools import setup, find_packages
from io import open
import re
@ -19,7 +19,7 @@ setup(name='pyTelegramBotAPI',
author='eternnoir',
author_email='eternnoir@gmail.com',
url='https://github.com/eternnoir/pyTelegramBotAPI',
packages=['telebot'],
packages = find_packages(exclude = ['tests', 'examples']),
license='GPL2',
keywords='telegram bot api tools',
install_requires=['requests'],
@ -33,5 +33,6 @@ setup(name='pyTelegramBotAPI',
'Programming Language :: Python :: 3',
'Environment :: Console',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
]
],
)

File diff suppressed because it is too large Load Diff

View File

@ -1533,11 +1533,17 @@ def upload_sticker_file(token, user_id, png_sticker):
def create_new_sticker_set(
token, user_id, name, title, emojis, png_sticker, tgs_sticker,
contains_masks=None, mask_position=None):
contains_masks=None, mask_position=None, webm_sticker=None):
method_url = 'createNewStickerSet'
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
stype = 'png_sticker' if png_sticker else 'tgs_sticker'
sticker = png_sticker or tgs_sticker
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
stype = 'webm_sticker'
else:
stype = 'tgs_sticker'
sticker = png_sticker or tgs_sticker or webm_sticker
files = None
if not util.is_string(sticker):
files = {stype: sticker}
@ -1547,14 +1553,22 @@ def create_new_sticker_set(
payload['contains_masks'] = contains_masks
if mask_position:
payload['mask_position'] = mask_position.to_json()
if webm_sticker:
payload['webm_sticker'] = webm_sticker
return _make_request(token, method_url, params=payload, files=files, method='post')
def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position):
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 = 'png_sticker' if png_sticker else 'tgs_sticker'
sticker = png_sticker or tgs_sticker
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
stype = 'webm_sticker'
else:
stype = 'tgs_sticker'
sticker = png_sticker or tgs_sticker or webm_sticker
files = None
if not util.is_string(sticker):
files = {stype: sticker}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,9 @@
from abc import ABC
from typing import Optional, Union
from telebot.asyncio_handler_backends import State
from telebot import types
class SimpleCustomFilter(ABC):
"""
@ -30,6 +35,95 @@ class AdvancedCustomFilter(ABC):
pass
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
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:param starts_with: string, True if object's text starts with passed string
:param ends_with: string, True if object's text starts with passed string
:param ignore_case: bool (default False), case insensitive
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
async def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
prepare_func = lambda string: str(string).lower()
else:
prepare_func = str
if self.equals:
result = prepare_func(self.equals) == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([prepare_func(i) in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(prepare_func(i)) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(prepare_func(i)) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
@ -42,8 +136,13 @@ class TextMatchFilter(AdvancedCustomFilter):
key = 'text'
async def check(self, message, text):
if type(text) is list:return message.text in text
else: return text == message.text
if isinstance(text, TextFilter):
return await text.check(message)
elif type(text) is list:
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
@ -58,7 +157,15 @@ class TextContainsFilter(AdvancedCustomFilter):
key = 'text_contains'
async def check(self, message, text):
return text in message.text
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
@ -70,8 +177,10 @@ class TextStartsFilter(AdvancedCustomFilter):
"""
key = 'text_startswith'
async def check(self, message, text):
return message.text.startswith(text)
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
@ -82,9 +191,11 @@ class ChatFilter(AdvancedCustomFilter):
"""
key = 'chat_id'
async def check(self, message, text):
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
@ -99,6 +210,7 @@ class ForwardFilter(SimpleCustomFilter):
async def check(self, message):
return message.forward_from_chat is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
@ -114,7 +226,6 @@ class IsReplyFilter(SimpleCustomFilter):
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
@ -127,8 +238,11 @@ class LanguageFilter(AdvancedCustomFilter):
key = 'language_code'
async def check(self, message, text):
if type(text) is list:return message.from_user.language_code in text
else: return message.from_user.language_code == text
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
@ -147,6 +261,7 @@ class IsAdminFilter(SimpleCustomFilter):
result = await self._bot.get_chat_member(message.chat.id, message.from_user.id)
return result.status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
@ -154,16 +269,51 @@ class StateFilter(AdvancedCustomFilter):
Example:
@bot.message_handler(state=1)
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
async def check(self, message, text):
result = await self.bot.current_states.current_state(message.from_user.id)
if result is False: return False
elif text == '*': return True
elif type(text) is list: return result in text
return result == text
if text == '*': return True
# needs to work with callbackquery
if isinstance(message, types.Message):
chat_id = message.chat.id
user_id = message.from_user.id
if isinstance(message, types.CallbackQuery):
chat_id = message.message.chat.id
user_id = message.from_user.id
message = message.message
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
if message.chat.type == 'group':
group_state = await self.bot.current_states.get_state(user_id, chat_id)
if group_state == text:
return True
elif type(text) is list and group_state in text:
return True
else:
user_state = await self.bot.current_states.get_state(user_id, chat_id)
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
class IsDigitFilter(SimpleCustomFilter):
"""

View File

@ -1,219 +1,56 @@
import os
import pickle
class StateMemory:
def __init__(self):
self._states = {}
async def add_state(self, chat_id, state):
"""
Add a state.
:param chat_id:
:param state: new state
"""
if chat_id in self._states:
self._states[chat_id]['state'] = state
else:
self._states[chat_id] = {'state': state,'data': {}}
async def current_state(self, chat_id):
"""Current state"""
if chat_id in self._states: return self._states[chat_id]['state']
else: return False
async def delete_state(self, chat_id):
"""Delete a state"""
self._states.pop(chat_id)
def get_data(self, chat_id):
return self._states[chat_id]['data']
async def set(self, chat_id, new_state):
"""
Set a new state for a user.
:param chat_id:
:param new_state: new_state of a user
"""
await self.add_state(chat_id,new_state)
async def add_data(self, chat_id, key, value):
result = self._states[chat_id]['data'][key] = value
return result
async def finish(self, chat_id):
"""
Finish(delete) state of a user.
:param chat_id:
"""
await self.delete_state(chat_id)
def retrieve_data(self, chat_id):
"""
Save input text.
Usage:
with bot.retrieve_data(message.chat.id) as data:
data['name'] = message.text
Also, at the end of your 'Form' you can get the name:
data['name']
"""
return StateContext(self, chat_id)
class StateFile:
"""
Class to save states in a file.
"""
def __init__(self, filename):
self.file_path = filename
async def add_state(self, chat_id, state):
"""
Add a state.
:param chat_id:
:param state: new state
"""
states_data = self.read_data()
if chat_id in states_data:
states_data[chat_id]['state'] = state
return await self.save_data(states_data)
else:
states_data[chat_id] = {'state': state,'data': {}}
return await self.save_data(states_data)
async def current_state(self, chat_id):
"""Current state."""
states_data = self.read_data()
if chat_id in states_data: return states_data[chat_id]['state']
else: return False
async def delete_state(self, chat_id):
"""Delete a state"""
states_data = self.read_data()
states_data.pop(chat_id)
await self.save_data(states_data)
def read_data(self):
"""
Read the data from file.
"""
file = open(self.file_path, 'rb')
states_data = pickle.load(file)
file.close()
return states_data
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs = self.file_path.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
async def save_data(self, new_data):
"""
Save data after editing.
:param new_data:
"""
with open(self.file_path, 'wb+') as state_file:
pickle.dump(new_data, state_file, protocol=pickle.HIGHEST_PROTOCOL)
return True
def get_data(self, chat_id):
return self.read_data()[chat_id]['data']
async def set(self, chat_id, new_state):
"""
Set a new state for a user.
:param chat_id:
:param new_state: new_state of a user
"""
await self.add_state(chat_id,new_state)
async def add_data(self, chat_id, key, value):
states_data = self.read_data()
result = states_data[chat_id]['data'][key] = value
await self.save_data(result)
return result
async def finish(self, chat_id):
"""
Finish(delete) state of a user.
:param chat_id:
"""
await self.delete_state(chat_id)
def retrieve_data(self, chat_id):
"""
Save input text.
Usage:
with bot.retrieve_data(message.chat.id) as data:
data['name'] = message.text
Also, at the end of your 'Form' you can get the name:
data['name']
"""
return StateFileContext(self, chat_id)
class StateContext:
"""
Class for data.
"""
def __init__(self , obj: StateMemory, chat_id) -> None:
self.obj = obj
self.chat_id = chat_id
self.data = obj.get_data(chat_id)
async def __aenter__(self):
return self.data
async def __aexit__(self, exc_type, exc_val, exc_tb):
return
class StateFileContext:
"""
Class for data.
"""
def __init__(self , obj: StateFile, chat_id) -> None:
self.obj = obj
self.chat_id = chat_id
self.data = None
async def __aenter__(self):
self.data = self.obj.get_data(self.chat_id)
return self.data
async def __aexit__(self, exc_type, exc_val, exc_tb):
old_data = self.obj.read_data()
for i in self.data:
old_data[self.chat_id]['data'][i] = self.data.get(i)
await self.obj.save_data(old_data)
return
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
"""
def __init__(self):
pass
async def pre_process(self, message, data):
raise NotImplementedError
async def post_process(self, message, data, exception):
raise NotImplementedError
class State:
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
def __init_subclass__(cls) -> None:
for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass

View File

@ -12,16 +12,8 @@ API_URL = 'https://api.telegram.org/bot{0}/{1}'
from datetime import datetime
import telebot
from telebot import util
from telebot import util, logger
class SessionBase:
def __init__(self) -> None:
self.session = None
async def _get_new_session(self):
self.session = aiohttp.ClientSession()
return self.session
session_manager = SessionBase()
proxy = None
session = None
@ -36,6 +28,30 @@ REQUEST_TIMEOUT = 10
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))
async def create_session(self):
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT))
return self.session
async def get_session(self):
if self.session.closed:
self.session = await self.create_session()
# noinspection PyProtectedMember
if not self.session._loop.is_running():
await self.session.close()
self.session = await self.create_session()
return self.session
session_manager = SessionManager()
async def _process_request(token, url, method='get', params=None, files=None, request_timeout=None):
params = prepare_data(params, files)
if request_timeout is None:
@ -43,19 +59,22 @@ async def _process_request(token, url, method='get', params=None, files=None, re
timeout = aiohttp.ClientTimeout(total=request_timeout)
got_result = False
current_try=0
async with await session_manager._get_new_session() as session:
while not got_result and current_try<MAX_RETRIES-1:
current_try +=1
try:
response = await session.request(method=method, url=API_URL.format(token, url), data=params, timeout=timeout)
session = await session_manager.get_session()
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:
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, response)
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:
pass
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__}')
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))
@ -143,8 +162,7 @@ async def download_file(token, file_path):
else:
# noinspection PyUnresolvedReferences
url = FILE_URL.format(token, file_path)
# TODO: rewrite this method
async with await session_manager._get_new_session() as session:
async with await session_manager.get_session() as session:
async with session.get(url, proxy=proxy) as response:
result = await response.read()
if response.status != 200:
@ -279,7 +297,7 @@ async def send_message(
return await _process_request(token, method_name, params=params)
# here shit begins
# methods
async def get_user_profile_photos(token, user_id, offset=None, limit=None):
method_url = r'getUserProfilePhotos'
@ -1183,7 +1201,7 @@ async def edit_message_caption(token, caption, chat_id=None, message_id=None, in
async def edit_message_media(token, media, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None):
method_url = r'editMessageMedia'
media_json, file = convert_input_media(media)
media_json, file = await convert_input_media(media)
payload = {'media': media_json}
if chat_id:
payload['chat_id'] = chat_id
@ -1480,11 +1498,17 @@ async def upload_sticker_file(token, user_id, png_sticker):
async def create_new_sticker_set(
token, user_id, name, title, emojis, png_sticker, tgs_sticker,
contains_masks=None, mask_position=None):
contains_masks=None, mask_position=None, webm_sticker=None):
method_url = 'createNewStickerSet'
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
stype = 'png_sticker' if png_sticker else 'tgs_sticker'
sticker = png_sticker or tgs_sticker
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
stype = 'webm_sticker'
else:
stype = 'tgs_sticker'
sticker = png_sticker or tgs_sticker or webm_sticker
files = None
if not util.is_string(sticker):
files = {stype: sticker}
@ -1494,21 +1518,33 @@ async def create_new_sticker_set(
payload['contains_masks'] = contains_masks
if mask_position:
payload['mask_position'] = mask_position.to_json()
if webm_sticker:
payload['webm_sticker'] = webm_sticker
return await _process_request(token, method_url, params=payload, files=files, method='post')
async def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position):
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 = 'png_sticker' if png_sticker else 'tgs_sticker'
sticker = png_sticker or tgs_sticker
stype = None
if png_sticker:
stype = 'png_sticker'
elif webm_sticker:
stype = 'webm_sticker'
else:
stype = 'tgs_sticker'
files = None
sticker = png_sticker or tgs_sticker or webm_sticker
if not util.is_string(sticker):
files = {stype: sticker}
else:
payload[stype] = sticker
if mask_position:
payload['mask_position'] = mask_position.to_json()
if webm_sticker:
payload['webm_sticker'] = webm_sticker
return await _process_request(token, method_url, params=payload, files=files, method='post')

View File

@ -0,0 +1,13 @@
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
from telebot.asyncio_storage.redis_storage import StateRedisStorage
from telebot.asyncio_storage.pickle_storage import StatePickleStorage
from telebot.asyncio_storage.base_storage import StateContext,StateStorageBase
__all__ = [
'StateStorageBase', 'StateContext',
'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage'
]

View File

@ -0,0 +1,68 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
async def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
async def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
async def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
async def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
async def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
async def get_state(self, chat_id, user_id):
raise NotImplementedError
async def save(self, chat_id, user_id, data):
raise NotImplementedError
class StateContext:
"""
Class for data.
"""
def __init__(self, obj, chat_id, user_id):
self.obj = obj
self.data = None
self.chat_id = chat_id
self.user_id = user_id
async def __aenter__(self):
self.data = copy.deepcopy(await self.obj.get_data(self.chat_id, self.user_id))
return self.data
async def __aexit__(self, exc_type, exc_val, exc_tb):
return await self.obj.save(self.chat_id, self.user_id, self.data)

View File

@ -0,0 +1,66 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
class StateMemoryStorage(StateStorageBase):
def __init__(self) -> None:
self.data = {}
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
async def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
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
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
return True
async def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
return True
return False
async def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
async def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
return True
return False
async def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data

View File

@ -0,0 +1,109 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import os
import pickle
class StatePickleStorage(StateStorageBase):
def __init__(self, file_path="./.state-save/states.pkl") -> None:
self.file_path = file_path
self.create_dir()
self.data = self.read()
async def convert_old_to_new(self):
# old looks like:
# {1: {'state': 'start', 'data': {'name': 'John'}}
# we should update old version pickle to new.
# new looks like:
# {1: {2: {'state': 'start', 'data': {'name': 'John'}}}}
new_data = {}
for key, value in self.data.items():
# this returns us id and dict with data and state
new_data[key] = {key: value} # convert this to new
# pass it to global data
self.data = new_data
self.update_data() # update data in file
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs = self.file_path.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
def read(self):
file = open(self.file_path, 'rb')
data = pickle.load(file)
file.close()
return data
def update_data(self):
file = open(self.file_path, 'wb+')
pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL)
file.close()
async def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
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
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()
return True
async def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
self.update_data()
return True
return False
async def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
async def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
self.update_data()
return True
return False
async def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
self.update_data()
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
self.update_data()

View File

@ -0,0 +1,171 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import json
redis_installed = True
try:
import aioredis
except:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
This class is for Redis storage.
This will work only for states.
To use it, just pass this class to:
TeleBot(storage=StateRedisStorage())
"""
def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_'):
if not redis_installed:
raise ImportError('AioRedis is not installed. Install it via "pip install aioredis"')
aioredis_version = tuple(map(int, aioredis.__version__.split(".")[0]))
if aioredis_version < (2,):
raise ImportError('Invalid aioredis version. Aioredis version should be >= 2.0.0')
self.redis = aioredis.Redis(host=host, port=port, db=db, password=password)
self.prefix = prefix
#self.con = Redis(connection_pool=self.redis) -> use this when necessary
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
async def get_record(self, key):
"""
Function to get record from database.
It has nothing to do with states.
Made for backend compatibility
"""
result = await self.redis.get(self.prefix+str(key))
if result: return json.loads(result)
return
async def set_record(self, key, value):
"""
Function to set record to database.
It has nothing to do with states.
Made for backend compatibility
"""
await self.redis.set(self.prefix+str(key), json.dumps(value))
return True
async def delete_record(self, key):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backend compatibility
"""
await self.redis.delete(self.prefix+str(key))
return True
async def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if isinstance(state, object):
state = state.name
if response:
if user_id in response:
response[user_id]['state'] = state
else:
response[user_id] = {'state': state, 'data': {}}
else:
response = {user_id: {'state': state, 'data': {}}}
await self.set_record(chat_id, response)
return True
async def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
del response[user_id]
if user_id == str(chat_id):
await self.delete_record(chat_id)
return True
else: await self.set_record(chat_id, response)
return True
return False
async def get_value(self, chat_id, user_id, key):
"""
Get value for a data of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
if key in response[user_id]['data']:
return response[user_id]['data'][key]
return None
async def get_state(self, chat_id, user_id):
"""
Get state of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['state']
return None
async def get_data(self, chat_id, user_id):
"""
Get data of particular user in a particular chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
"""
Reset data of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = {}
await self.set_record(chat_id, response)
return True
async def set_data(self, chat_id, user_id, key, value):
"""
Set data without interactive data.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'][key] = value
await self.set_record(chat_id, response)
return True
return False
def get_interactive_data(self, chat_id, user_id):
"""
Get Data in interactive way.
You can use with() with this function.
"""
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = dict(data, **response[user_id]['data'])
await self.set_record(chat_id, response)
return True

View File

@ -10,6 +10,7 @@ class CallbackDataFilter:
def check(self, query):
"""
Checks if query.data appropriates to specified config
:param query: telebot.types.CallbackQuery
:return: bool
"""
@ -50,6 +51,7 @@ class CallbackData:
def new(self, *args, **kwargs) -> str:
"""
Generate callback data
:param args: positional parameters of CallbackData instance parts
:param kwargs: named parameters
:return: str
@ -87,6 +89,7 @@ class CallbackData:
def parse(self, callback_data: str) -> typing.Dict[str, str]:
"""
Parse data from the callback data
:param callback_data: string, use to telebot.types.CallbackQuery to parse it from string to a dict
:return: dict parsed from callback data
"""

View File

@ -1,4 +1,11 @@
from abc import ABC
from typing import Optional, Union
from telebot.handler_backends import State
from telebot import types
class SimpleCustomFilter(ABC):
"""
@ -30,6 +37,100 @@ class AdvancedCustomFilter(ABC):
pass
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
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:param starts_with: string, True if object's text starts with passed string
:param ends_with: string, True if object's text starts with passed string
:param ignore_case: bool (default False), case insensitive
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name: str):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
if self.equals:
self.equals = self.equals.lower()
elif self.contains:
self.contains = tuple(map(str.lower, self.contains))
elif self.starts_with:
self.starts_with = tuple(map(str.lower, self.starts_with))
elif self.ends_with:
self.ends_with = tuple(map(str.lower, self.ends_with))
if self.equals:
result = self.equals == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([i in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(i) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(i) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
@ -42,8 +143,13 @@ class TextMatchFilter(AdvancedCustomFilter):
key = 'text'
def check(self, message, text):
if type(text) is list:return message.text in text
else: return text == message.text
if isinstance(text, TextFilter):
return text.check(message)
elif type(text) is list:
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
@ -58,7 +164,15 @@ class TextContainsFilter(AdvancedCustomFilter):
key = 'text_contains'
def check(self, message, text):
return text in message.text
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
@ -70,8 +184,10 @@ class TextStartsFilter(AdvancedCustomFilter):
"""
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
@ -82,9 +198,11 @@ class ChatFilter(AdvancedCustomFilter):
"""
key = 'chat_id'
def check(self, message, text):
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
@ -99,6 +217,7 @@ class ForwardFilter(SimpleCustomFilter):
def check(self, message):
return message.forward_from_chat is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
@ -114,7 +233,6 @@ class IsReplyFilter(SimpleCustomFilter):
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
@ -127,8 +245,11 @@ class LanguageFilter(AdvancedCustomFilter):
key = 'language_code'
def check(self, message, text):
if type(text) is list:return message.from_user.language_code in text
else: return message.from_user.language_code == text
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
@ -146,6 +267,7 @@ class IsAdminFilter(SimpleCustomFilter):
def check(self, message):
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
@ -153,15 +275,53 @@ class StateFilter(AdvancedCustomFilter):
Example:
@bot.message_handler(state=1)
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
def check(self, message, text):
if self.bot.current_states.current_state(message.from_user.id) is False: return False
elif text == '*': return True
elif type(text) is list: return self.bot.current_states.current_state(message.from_user.id) in text
return self.bot.current_states.current_state(message.from_user.id) == text
if text == '*': return True
# needs to work with callbackquery
if isinstance(message, types.Message):
chat_id = message.chat.id
user_id = message.from_user.id
if isinstance(message, types.CallbackQuery):
chat_id = message.message.chat.id
user_id = message.from_user.id
message = message.message
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
if message.chat.type == 'group':
group_state = self.bot.current_states.get_state(user_id, chat_id)
if group_state == text:
return True
elif type(text) is list and group_state in text:
return True
else:
user_state = self.bot.current_states.get_state(user_id, chat_id)
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
class IsDigitFilter(SimpleCustomFilter):
"""

View File

@ -3,6 +3,11 @@ import pickle
import threading
from telebot import apihelper
try:
from redis import Redis
redis_installed = True
except:
redis_installed = False
class HandlerBackend(object):
@ -116,7 +121,8 @@ class FileHandlerBackend(HandlerBackend):
class RedisHandlerBackend(HandlerBackend):
def __init__(self, handlers=None, host='localhost', port=6379, db=0, prefix='telebot', password=None):
super(RedisHandlerBackend, self).__init__(handlers)
from redis import Redis
if not redis_installed:
raise Exception("Redis is not installed. Install it via 'pip install redis'")
self.prefix = prefix
self.redis = Redis(host, port, db, password)
@ -143,197 +149,58 @@ class RedisHandlerBackend(HandlerBackend):
return handlers
class StateMemory:
class State:
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
def __init_subclass__(cls) -> None:
for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
"""
def __init__(self):
self._states = {}
pass
def add_state(self, chat_id, state):
"""
Add a state.
:param chat_id:
:param state: new state
"""
if chat_id in self._states:
self._states[chat_id]['state'] = state
else:
self._states[chat_id] = {'state': state,'data': {}}
def pre_process(self, message, data):
raise NotImplementedError
def current_state(self, chat_id):
"""Current state"""
if chat_id in self._states: return self._states[chat_id]['state']
else: return False
def delete_state(self, chat_id):
"""Delete a state"""
self._states.pop(chat_id)
def get_data(self, chat_id):
return self._states[chat_id]['data']
def set(self, chat_id, new_state):
"""
Set a new state for a user.
:param chat_id:
:param new_state: new_state of a user
"""
self.add_state(chat_id,new_state)
def add_data(self, chat_id, key, value):
result = self._states[chat_id]['data'][key] = value
return result
def finish(self, chat_id):
"""
Finish(delete) state of a user.
:param chat_id:
"""
self.delete_state(chat_id)
def retrieve_data(self, chat_id):
"""
Save input text.
Usage:
with bot.retrieve_data(message.chat.id) as data:
data['name'] = message.text
Also, at the end of your 'Form' you can get the name:
data['name']
"""
return StateContext(self, chat_id)
def post_process(self, message, data, exception):
raise NotImplementedError
class StateFile:
class SkipHandler:
"""
Class to save states in a file.
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self, filename):
self.file_path = filename
def add_state(self, chat_id, state):
"""
Add a state.
:param chat_id:
:param state: new state
"""
states_data = self.read_data()
if chat_id in states_data:
states_data[chat_id]['state'] = state
return self.save_data(states_data)
else:
states_data[chat_id] = {'state': state,'data': {}}
return self.save_data(states_data)
def __init__(self) -> None:
pass
def current_state(self, chat_id):
"""Current state."""
states_data = self.read_data()
if chat_id in states_data: return states_data[chat_id]['state']
else: return False
def delete_state(self, chat_id):
"""Delete a state"""
states_data = self.read_data()
states_data.pop(chat_id)
self.save_data(states_data)
def read_data(self):
"""
Read the data from file.
"""
file = open(self.file_path, 'rb')
states_data = pickle.load(file)
file.close()
return states_data
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs = self.file_path.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
def save_data(self, new_data):
"""
Save data after editing.
:param new_data:
"""
with open(self.file_path, 'wb+') as state_file:
pickle.dump(new_data, state_file, protocol=pickle.HIGHEST_PROTOCOL)
return True
def get_data(self, chat_id):
return self.read_data()[chat_id]['data']
def set(self, chat_id, new_state):
"""
Set a new state for a user.
:param chat_id:
:param new_state: new_state of a user
"""
self.add_state(chat_id,new_state)
def add_data(self, chat_id, key, value):
states_data = self.read_data()
result = states_data[chat_id]['data'][key] = value
self.save_data(result)
return result
def finish(self, chat_id):
"""
Finish(delete) state of a user.
:param chat_id:
"""
self.delete_state(chat_id)
def retrieve_data(self, chat_id):
"""
Save input text.
Usage:
with bot.retrieve_data(message.chat.id) as data:
data['name'] = message.text
Also, at the end of your 'Form' you can get the name:
data['name']
"""
return StateFileContext(self, chat_id)
class StateContext:
class CancelUpdate:
"""
Class for data.
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self , obj: StateMemory, chat_id) -> None:
self.obj = obj
self.chat_id = chat_id
self.data = obj.get_data(chat_id)
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
return
class StateFileContext:
"""
Class for data.
"""
def __init__(self , obj: StateFile, chat_id) -> None:
self.obj = obj
self.chat_id = chat_id
self.data = self.obj.get_data(self.chat_id)
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
old_data = self.obj.read_data()
for i in self.data:
old_data[self.chat_id]['data'][i] = self.data.get(i)
self.obj.save_data(old_data)
return
def __init__(self) -> None:
pass

View File

@ -0,0 +1,13 @@
from telebot.storage.memory_storage import StateMemoryStorage
from telebot.storage.redis_storage import StateRedisStorage
from telebot.storage.pickle_storage import StatePickleStorage
from telebot.storage.base_storage import StateContext,StateStorageBase
__all__ = [
'StateStorageBase', 'StateContext',
'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage'
]

View File

@ -0,0 +1,65 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
def get_state(self, chat_id, user_id):
raise NotImplementedError
def save(self, chat_id, user_id, data):
raise NotImplementedError
class StateContext:
"""
Class for data.
"""
def __init__(self , obj, chat_id, user_id) -> None:
self.obj = obj
self.data = copy.deepcopy(obj.get_data(chat_id, user_id))
self.chat_id = chat_id
self.user_id = user_id
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
return self.obj.save(self.chat_id, self.user_id, self.data)

View File

@ -0,0 +1,67 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
class StateMemoryStorage(StateStorageBase):
def __init__(self) -> None:
self.data = {}
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
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
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
return True
def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
return True
return False
def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
return True
return False
def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data

View File

@ -0,0 +1,115 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
import os
import pickle
class StatePickleStorage(StateStorageBase):
# noinspection PyMissingConstructor
def __init__(self, file_path="./.state-save/states.pkl") -> None:
self.file_path = file_path
self.create_dir()
self.data = self.read()
def convert_old_to_new(self):
"""
Use this function to convert old storage to new storage.
This function is for people who was using pickle storage
that was in version <=4.3.1.
"""
# old looks like:
# {1: {'state': 'start', 'data': {'name': 'John'}}
# we should update old version pickle to new.
# new looks like:
# {1: {2: {'state': 'start', 'data': {'name': 'John'}}}}
new_data = {}
for key, value in self.data.items():
# this returns us id and dict with data and state
new_data[key] = {key: value} # convert this to new
# pass it to global data
self.data = new_data
self.update_data() # update data in file
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs = self.file_path.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
def read(self):
file = open(self.file_path, 'rb')
data = pickle.load(file)
file.close()
return data
def update_data(self):
file = open(self.file_path, 'wb+')
pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL)
file.close()
def set_state(self, chat_id, user_id, state):
if isinstance(state, object):
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
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()
return True
def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
self.update_data()
return True
return False
def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
self.update_data()
return True
return False
def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
self.update_data()
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
self.update_data()

View File

@ -0,0 +1,180 @@
from pyclbr import Class
from telebot.storage.base_storage import StateStorageBase, StateContext
import json
redis_installed = True
try:
from redis import Redis, ConnectionPool
except:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
This class is for Redis storage.
This will work only for states.
To use it, just pass this class to:
TeleBot(storage=StateRedisStorage())
"""
def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_'):
self.redis = ConnectionPool(host=host, port=port, db=db, password=password)
#self.con = Redis(connection_pool=self.redis) -> use this when necessary
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
self.prefix = prefix
if not redis_installed:
raise Exception("Redis is not installed. Install it via 'pip install redis'")
def get_record(self, key):
"""
Function to get record from database.
It has nothing to do with states.
Made for backend compatibility
"""
connection = Redis(connection_pool=self.redis)
result = connection.get(self.prefix+str(key))
connection.close()
if result: return json.loads(result)
return
def set_record(self, key, value):
"""
Function to set record to database.
It has nothing to do with states.
Made for backend compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.set(self.prefix+str(key), json.dumps(value))
connection.close()
return True
def delete_record(self, key):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backend compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.delete(self.prefix+str(key))
connection.close()
return True
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if isinstance(state, object):
state = state.name
if response:
if user_id in response:
response[user_id]['state'] = state
else:
response[user_id] = {'state': state, 'data': {}}
else:
response = {user_id: {'state': state, 'data': {}}}
self.set_record(chat_id, response)
return True
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
del response[user_id]
if user_id == str(chat_id):
self.delete_record(chat_id)
return True
else: self.set_record(chat_id, response)
return True
return False
def get_value(self, chat_id, user_id, key):
"""
Get value for a data of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
if key in response[user_id]['data']:
return response[user_id]['data'][key]
return None
def get_state(self, chat_id, user_id):
"""
Get state of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['state']
return None
def get_data(self, chat_id, user_id):
"""
Get data of particular user in a particular chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['data']
return None
def reset_data(self, chat_id, user_id):
"""
Reset data of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = {}
self.set_record(chat_id, response)
return True
def set_data(self, chat_id, user_id, key, value):
"""
Set data without interactive data.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'][key] = value
self.set_record(chat_id, response)
return True
return False
def get_interactive_data(self, chat_id, user_id):
"""
Get Data in interactive way.
You can use with() with this function.
"""
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = dict(data, **response[user_id]['data'])
self.set_record(chat_id, response)
return True

View File

@ -69,6 +69,7 @@ class JsonDeserializable(object):
"""
Checks whether json_type is a dict or a string. If it is already a dict, it is returned as-is.
If it is not, it is converted to a dict by means of json.loads(json_type)
:param json_type: input json or parsed dict
:param dict_copy: if dict is passed and it is changed outside - should be True!
:return: Dictionary parsed from json or original dict
@ -943,6 +944,7 @@ class ReplyKeyboardMarkup(JsonSerializable):
when row_width is set to 1.
When row_width is set to 2, the following is the result of this function: {keyboard: [["A", "B"], ["C"]]}
See https://core.telegram.org/bots/api#replykeyboardmarkup
:param args: KeyboardButton to append to the keyboard
:param row_width: width of row
:return: self, to allow function chaining.
@ -974,6 +976,7 @@ class ReplyKeyboardMarkup(JsonSerializable):
Adds a list of KeyboardButton to the keyboard. This function does not consider row_width.
ReplyKeyboardMarkup#row("A")#row("B", "C")#to_json() outputs '{keyboard: [["A"], ["B", "C"]]}'
See https://core.telegram.org/bots/api#replykeyboardmarkup
:param args: strings
:return: self, to allow function chaining.
"""
@ -1041,9 +1044,9 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def __init__(self, keyboard=None, row_width=3):
"""
This object represents an inline keyboard that appears
right next to the message it belongs to.
right next to the message it belongs to.
:return:
:return: None
"""
if row_width > self.max_row_keys:
# Todo: Will be replaced with Exception in future releases
@ -1058,10 +1061,10 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
This method adds buttons to the keyboard without exceeding row_width.
E.g. InlineKeyboardMarkup.add("A", "B", "C") yields the json result:
{keyboard: [["A"], ["B"], ["C"]]}
{keyboard: [["A"], ["B"], ["C"]]}
when row_width is set to 1.
When row_width is set to 2, the result:
{keyboard: [["A", "B"], ["C"]]}
{keyboard: [["A", "B"], ["C"]]}
See https://core.telegram.org/bots/api#inlinekeyboardmarkup
:param args: Array of InlineKeyboardButton to append to the keyboard
@ -1085,10 +1088,10 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def row(self, *args):
"""
Adds a list of InlineKeyboardButton to the keyboard.
This method does not consider row_width.
This method does not consider row_width.
InlineKeyboardMarkup.row("A").row("B", "C").to_json() outputs:
'{keyboard: [["A"], ["B", "C"]]}'
'{keyboard: [["A"], ["B", "C"]]}'
See https://core.telegram.org/bots/api#inlinekeyboardmarkup
:param args: Array of InlineKeyboardButton to append to the keyboard
@ -1100,7 +1103,7 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def to_json(self):
"""
Converts this object to its json representation
following the Telegram API guidelines described here:
following the Telegram API guidelines described here:
https://core.telegram.org/bots/api#inlinekeyboardmarkup
:return:
"""
@ -2399,6 +2402,7 @@ class ShippingOption(JsonSerializable):
def add_price(self, *args):
"""
Add LabeledPrice to ShippingOption
:param args: LabeledPrices
"""
for price in args:
@ -2484,10 +2488,11 @@ class StickerSet(JsonDeserializable):
obj['thumb'] = None
return cls(**obj)
def __init__(self, name, title, is_animated, contains_masks, stickers, thumb=None, **kwargs):
def __init__(self, name, title, is_animated, is_video, contains_masks, stickers, thumb=None, **kwargs):
self.name: str = name
self.title: str = title
self.is_animated: bool = is_animated
self.is_video: bool = is_video
self.contains_masks: bool = contains_masks
self.stickers: List[Sticker] = stickers
self.thumb: PhotoSize = thumb
@ -2507,12 +2512,13 @@ class Sticker(JsonDeserializable):
return cls(**obj)
def __init__(self, file_id, file_unique_id, width, height, is_animated,
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, **kwargs):
self.file_id: str = file_id
self.file_unique_id: str = file_unique_id
self.width: int = width
self.height: int = height
self.is_animated: bool = is_animated
self.is_video: bool = is_video
self.thumb: PhotoSize = thumb
self.emoji: str = emoji
self.set_name: str = set_name

View File

@ -4,7 +4,6 @@ import re
import string
import threading
import traceback
import warnings
from typing import Any, Callable, List, Dict, Optional, Union
# noinspection PyPep8Naming
@ -22,6 +21,7 @@ try:
# noinspection PyPackageRequirements
from PIL import Image
from io import BytesIO
pil_imported = True
except:
pil_imported = False
@ -40,16 +40,17 @@ 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',
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
]
update_types = [
"update_id", "message", "edited_message", "channel_post", "edited_channel_post", "inline_query",
"chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer",
"update_id", "message", "edited_message", "channel_post", "edited_channel_post", "inline_query",
"chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer",
"my_chat_member", "chat_member", "chat_join_request"
]
class WorkerThread(threading.Thread):
count = 0
@ -115,7 +116,8 @@ class WorkerThread(threading.Thread):
class ThreadPool:
def __init__(self, num_threads=2):
def __init__(self, telebot, num_threads=2):
self.telebot = telebot
self.tasks = Queue.Queue()
self.workers = [WorkerThread(self.on_exception, self.tasks) for _ in range(num_threads)]
self.num_threads = num_threads
@ -127,8 +129,13 @@ class ThreadPool:
self.tasks.put((func, args, kwargs))
def on_exception(self, worker_thread, exc_info):
self.exception_info = exc_info
self.exception_event.set()
if self.telebot.exception_handler is not None:
handled = self.telebot.exception_handler.handle(exc_info)
else:
handled = False
if not handled:
self.exception_info = exc_info
self.exception_event.set()
worker_thread.continue_event.set()
def raise_exceptions(self):
@ -212,15 +219,16 @@ def pil_image_to_file(image, extension='JPEG', quality='web_low'):
photoBuffer = BytesIO()
image.convert('RGB').save(photoBuffer, extension, quality=quality)
photoBuffer.seek(0)
return photoBuffer
else:
raise RuntimeError('PIL module is not imported')
def is_command(text: str) -> bool:
"""
r"""
Checks if `text` is a command. Telegram chat commands start with the '/' character.
:param text: Text to check.
:return: True if `text` is a command, else False.
"""
@ -276,7 +284,7 @@ def split_string(text: str, chars_per_string: int) -> List[str]:
def smart_split(text: str, chars_per_string: int=MAX_MESSAGE_LENGTH) -> List[str]:
"""
r"""
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
This is very useful for splitting one giant message into multiples.
If `chars_per_string` > 4096: `chars_per_string` = 4096.
@ -315,7 +323,7 @@ def escape(text: str) -> str:
:param text: the text to escape
:return: the escaped text
"""
chars = {"&": "&amp;", "<": "&lt;", ">": "&gt"}
chars = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
for old, new in chars.items(): text = text.replace(old, new)
return text
@ -333,7 +341,7 @@ def user_link(user: types.User, include_id: bool=False) -> str:
:return: HTML user link
"""
name = escape(user.first_name)
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
@ -343,24 +351,27 @@ def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.I
This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)'
Example:
quick_markup({
'Twitter': {'url': 'https://twitter.com'},
'Facebook': {'url': 'https://facebook.com'},
'Back': {'callback_data': 'whatever'}
}, row_width=2):
returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
and a back button below
kwargs can be:
{
'url': None,
'callback_data': None,
'switch_inline_query': None,
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None
}
.. code-block:: python
quick_markup({
'Twitter': {'url': 'https://twitter.com'},
'Facebook': {'url': 'https://facebook.com'},
'Back': {'callback_data': 'whatever'}
}, row_width=2):
# returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
# and a back button below
# kwargs can be:
{
'url': None,
'callback_data': None,
'switch_inline_query': None,
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None
}
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
:param row_width: int row width
@ -408,6 +419,7 @@ def OrEvent(*events):
def busy_wait():
while not or_event.is_set():
# noinspection PyProtectedMember
or_event._wait(3)
for e in events:
@ -441,6 +453,7 @@ def deprecated(warn: bool=True, alternative: Optional[Callable]=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
"""
@ -471,16 +484,22 @@ def webhook_google_functions(bot, request):
else:
return 'Bot ON'
def antiflood(function, *args, **kwargs):
"""
Use this function inside loops in order to avoid getting TooManyRequests error.
Example:
from telebot.util import antiflood
for chat_id in chat_id_list:
.. code-block:: python3
from telebot.util import antiflood
for chat_id in chat_id_list:
msg = antiflood(bot.send_message, chat_id, text)
You want get the
:param function:
:param args:
:param kwargs:
:return: None
"""
from telebot.apihelper import ApiTelegramException
from time import sleep

View File

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

0
tests/__init__.py Normal file
View File

View File

@ -464,12 +464,12 @@ class TestTeleBot:
assert new_msg.message_id
def test_antiflood(self):
text = "Flooding"
text = "Testing antiflood function"
tb = telebot.TeleBot(TOKEN)
i = -1
for i in range(0,100):
for i in range(0,200):
util.antiflood(tb.send_message, CHAT_ID, text)
assert i
assert i == 199
@staticmethod
def create_text_message(text):

View File

@ -80,7 +80,7 @@ def test_json_Message_Audio():
def test_json_Message_Sticker():
json_string = r'{"message_id": 21552, "from": {"id": 590740002, "is_bot": false, "first_name": "⚜️ Ƥυrуα ⚜️", "username": "Purya", "language_code": "en"}, "chat": {"id": -1001309982000, "title": "123", "type": "supergroup"}, "date": 1594068909, "sticker": {"width": 368, "height": 368, "emoji": "🤖", "set_name": "ipuryapack", "is_animated": false, "thumb": {"file_id": "AAMCBAADHQJOFL7mAAJUMF8Dj62hpmDhpRAYvkc8CtIqipolAAJ8AAPA-8cF9yxjgjkLS97A0D4iXQARtQAHbQADHy4AAhoE", "file_unique_id": "AQADwNA-Il0AAx8uAAI", "file_size": 7776, "width": 60, "height": 60}, "file_id": "CAACAgQAAx0CThS-5gACVDBfA4-toaZg4aUQGL5HWerSKoqaJQACArADwPvHBfcsY4I5C3feGgQ", "file_unique_id": "AgADfAADsPvHWQ", "file_size": 14602}}'
json_string = r'{"message_id": 21552, "from": {"id": 590740002, "is_bot": false, "first_name": "⚜️ Ƥυrуα ⚜️", "username": "Purya", "language_code": "en"}, "chat": {"id": -1001309982000, "title": "123", "type": "supergroup"}, "date": 1594068909, "sticker": {"width": 368, "height": 368, "emoji": "🤖", "set_name": "ipuryapack", "is_animated": false, "is_video": true, "thumb": {"file_id": "AAMCBAADHQJOFL7mAAJUMF8Dj62hpmDhpRAYvkc8CtIqipolAAJ8AAPA-8cF9yxjgjkLS97A0D4iXQARtQAHbQADHy4AAhoE", "file_unique_id": "AQADwNA-Il0AAx8uAAI", "file_size": 7776, "width": 60, "height": 60}, "file_id": "CAACAgQAAx0CThS-5gACVDBfA4-toaZg4aUQGL5HWerSKoqaJQACArADwPvHBfcsY4I5C3feGgQ", "file_unique_id": "AgADfAADsPvHWQ", "file_size": 14602}}'
msg = types.Message.de_json(json_string)
assert msg.sticker.height == 368
assert msg.sticker.thumb.height == 60
@ -88,7 +88,7 @@ def test_json_Message_Sticker():
def test_json_Message_Sticker_without_thumb():
json_string = r'{"message_id": 21552, "from": {"id": 590740002, "is_bot": false, "first_name": "⚜️ Ƥυrуα ⚜️", "username": "Purya", "language_code": "en"}, "chat": {"id": -1001309982000, "title": "123", "type": "supergroup"}, "date": 1594068909, "sticker": {"width": 368, "height": 368, "emoji": "🤖", "set_name": "ipuryapack", "is_animated": false, "file_id": "CAACAgQAAx0CThS-5gACVDBfA4-toaZg4aUQGL5HWerSKoqaJQACArADwPvHBfcsY4I5C3feGgQ", "file_unique_id": "AgADfAADsPvHWQ", "file_size": 14602}}'
json_string = r'{"message_id": 21552, "from": {"id": 590740002, "is_bot": false, "first_name": "⚜️ Ƥυrуα ⚜️", "username": "Purya", "language_code": "en"}, "chat": {"id": -1001309982000, "title": "123", "type": "supergroup"}, "date": 1594068909, "sticker": {"width": 368, "height": 368, "emoji": "🤖", "set_name": "ipuryapack", "is_animated": false, "is_video": true, "file_id": "CAACAgQAAx0CThS-5gACVDBfA4-toaZg4aUQGL5HWerSKoqaJQACArADwPvHBfcsY4I5C3feGgQ", "file_unique_id": "AgADfAADsPvHWQ", "file_size": 14602}}'
msg = types.Message.de_json(json_string)
assert msg.sticker.height == 368
assert msg.sticker.thumb is None