mirror of
https://github.com/eternnoir/pyTelegramBotAPI.git
synced 2023-08-10 21:12:57 +03:00
Compare commits
243 Commits
7588c9fb9f
...
4.5.1
Author | SHA1 | Date | |
---|---|---|---|
efb1b44e59 | |||
2c8793b794 | |||
146fd57b10 | |||
8a12ae3565 | |||
2d8c2312e3 | |||
f9cd0d7e08 | |||
59cd1a00e7 | |||
836130a718 | |||
a7db2d8d9c | |||
c022d49996 | |||
825827cb1e | |||
cd92b70d6b | |||
617c990994 | |||
9b959373db | |||
76c0197ab7 | |||
7d9658b062 | |||
db0c946780 | |||
c6202da36f | |||
532011138c | |||
191164cba0 | |||
5688aaa03b | |||
88a76c0a15 | |||
e1dc6d7beb | |||
730d11012d | |||
b43b636ba0 | |||
a7ca6c057e | |||
bd002c6429 | |||
453df01f26 | |||
24ae38cca6 | |||
3b386965ea | |||
5077289d0d | |||
a54b21cb50 | |||
fa80cb0002 | |||
ad5b92b650 | |||
9b1b324ab4 | |||
e444bc2a0b | |||
dd25432359 | |||
cfbbfe84ad | |||
b25d2846e9 | |||
ab64e17464 | |||
3a5db47c1b | |||
4812dcb02b | |||
b146df346d | |||
5f2713bcfb | |||
a1bf961fd2 | |||
a8451a5e30 | |||
9417c49d8e | |||
1a40f1da7a | |||
5e28f27764 | |||
43d0e10ba4 | |||
9652fdbecb | |||
110575ca40 | |||
22b4e636e2 | |||
1688a466f4 | |||
625da4cdd9 | |||
a9e0f5b7b0 | |||
7b1b1a7caa | |||
a6477541c0 | |||
b652a9f6dc | |||
73b2813512 | |||
ace28983b6 | |||
e82675320c | |||
1e88671799 | |||
1cdf9640d7 | |||
91badb53e5 | |||
477d02468d | |||
9f3a270fae | |||
244b058648 | |||
886c9b9bc0 | |||
41025ba97b | |||
4875bb6188 | |||
5f91c3d4e6 | |||
7b62915a5b | |||
05c3cb2c1d | |||
60a96d1400 | |||
dcd0df93da | |||
f15101fc6f | |||
5f03253398 | |||
8fab55e937 | |||
60a23665cb | |||
b292b275cb | |||
403028bf35 | |||
3dda5cff06 | |||
dd589e2490 | |||
c8fb83c97c | |||
854f6a9506 | |||
be0557c2b5 | |||
436422e4da | |||
fc72576aaa | |||
f69a2ba044 | |||
c45e06c694 | |||
78bdf1ca4e | |||
3c7d3c0196 | |||
441a5793cc | |||
388477686b | |||
4f654d9e12 | |||
ac12d0fc02 | |||
b8ebe4fd58 | |||
c84896391e | |||
995e28e9d8 | |||
1bfc082d46 | |||
1a35bbb127 | |||
e585c77830 | |||
5ca92ff637 | |||
f4c76553ed | |||
75baf6dd96 | |||
301b9288a4 | |||
70b9fc86d2 | |||
dde9cd323c | |||
01a6827542 | |||
b960a9e574 | |||
102fe3a8fb | |||
292df419ba | |||
7993e1d1c9 | |||
7309f92c36 | |||
7875ff293d | |||
4adac4d852 | |||
38bff65caf | |||
9ecadf1bc1 | |||
5d7ae385ec | |||
74e9780b30 | |||
9b20f41ece | |||
967309120e | |||
94be2abdbd | |||
6c31b53cd9 | |||
9bfc0b2c6f | |||
fc374ec57a | |||
7a8e60ddc2 | |||
7f43f26886 | |||
4521982837 | |||
30c43b557c | |||
10b5886dcc | |||
93b97fc3fe | |||
1f6e60fd74 | |||
5337d4838d | |||
ae5d183db0 | |||
0d85a34551 | |||
002c608d45 | |||
ec766a3e43 | |||
0ef8d04ed2 | |||
3a86916e72 | |||
b41435f407 | |||
f689d90815 | |||
966f2e7ef7 | |||
9075430210 | |||
68095ad69a | |||
8c3d1e608c | |||
6822f18cbb | |||
6e4f2e19d6 | |||
8bbd062d13 | |||
5f7ccc8c9b | |||
5b1483f646 | |||
3cd86d0e93 | |||
a893fbc358 | |||
6fd2a38fe9 | |||
b89ecb3e5a | |||
2e5590b566 | |||
733bb2ebbb | |||
64a22457e2 | |||
0c8e94d2c6 | |||
b9436821e0 | |||
a8af9120de | |||
0655a1f6b6 | |||
97dbedaa54 | |||
4028b44d07 | |||
661218c7e3 | |||
cd4a9add68 | |||
7d2915c7f9 | |||
ce56a035b5 | |||
9fa79aabc0 | |||
62fad9ca3a | |||
388f055643 | |||
71be20636a | |||
3b38d1b46e | |||
1e0c2ea633 | |||
4e7652be7a | |||
723075d2da | |||
7ba021871a | |||
d7cb819502 | |||
5ee2aa77c6 | |||
80cf5d8d5b | |||
69277400b7 | |||
8d380b4913 | |||
23d20e0753 | |||
6fc7beba57 | |||
8d49d22074 | |||
6aa97d055f | |||
e55938e23a | |||
4166fb229e | |||
2e9947277a | |||
c350ea0ced | |||
588b5c4d89 | |||
91d0877c61 | |||
8045ad56ea | |||
124b07ee44 | |||
195974ddc1 | |||
2b081b42bb | |||
321d241483 | |||
ad4ff5835e | |||
a3cda2e0ff | |||
cf2eb1fec7 | |||
7eb759d1fd | |||
a07bf86c30 | |||
64c4aca3b7 | |||
40465643b9 | |||
56fbf491bc | |||
685c071056 | |||
fdbc0e6a61 | |||
7fe8d27686 | |||
9050f4af1f | |||
9140044956 | |||
2e6b6bda53 | |||
8d8aa5a380 | |||
ae2dbd00fa | |||
6550a5d745 | |||
593b27358b | |||
a51ff0f100 | |||
a96bc802bc | |||
df8f34e726 | |||
00998ac9c8 | |||
3f243c64ca | |||
034241ba31 | |||
ed6fb57cb5 | |||
b71507387f | |||
e7d0ec1f6c | |||
b3b318fd28 | |||
d334f5cb8d | |||
7f06424980 | |||
7490aa0d26 | |||
e59e2ee2ee | |||
f25dcad10c | |||
24a9491ec0 | |||
744549defe | |||
c86fc4c3fa | |||
943396767c | |||
13fffe58a1 | |||
7fe60e19ef | |||
ba9bf17f46 | |||
373ee4b45b | |||
e5f0ba67fc | |||
096d58ae71 | |||
ed5b47cb96 | |||
e92946301f |
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
|
||||
|
2
.github/workflows/setup_python.yml
vendored
2
.github/workflows/setup_python.yml
vendored
@ -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
3
.gitignore
vendored
@ -64,3 +64,6 @@ testMain.py
|
||||
#VS Code
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
# documentation
|
||||
_build/
|
||||
|
19
.readthedocs.yml
Normal file
19
.readthedocs.yml
Normal 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
|
65
README.md
65
README.md
@ -1,16 +1,20 @@
|
||||
|
||||
[](https://pypi.python.org/pypi/pyTelegramBotAPI)
|
||||
[](https://pypi.python.org/pypi/pyTelegramBotAPI)
|
||||
[](https://pytba.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://travis-ci.org/eternnoir/pyTelegramBotAPI)
|
||||
[](https://pypi.org/project/pyTelegramBotAPI/)
|
||||
[](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-7-2021">5.5</a>!
|
||||
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#april-16-2022">6.0</a>!
|
||||
|
||||
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
|
||||
|
||||
## Contents
|
||||
|
||||
* [Getting started](#getting-started)
|
||||
@ -58,7 +62,9 @@
|
||||
* [How can I distinguish a User and a GroupChat in message.chat?](#how-can-i-distinguish-a-user-and-a-groupchat-in-messagechat)
|
||||
* [How can I handle reocurring ConnectionResetErrors?](#how-can-i-handle-reocurring-connectionreseterrors)
|
||||
* [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
|
||||
@ -66,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
|
||||
@ -78,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
|
||||
|
||||
@ -343,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.
|
||||
@ -641,6 +656,8 @@ telebot.logger.setLevel(logging.DEBUG) # Outputs debug messages to console.
|
||||
```
|
||||
|
||||
### Proxy
|
||||
For sync:
|
||||
|
||||
You can use proxy for request. `apihelper.proxy` object will use by call `requests` proxies argument.
|
||||
|
||||
```python
|
||||
@ -655,6 +672,14 @@ If you want to use socket5 proxy you need install dependency `pip install reques
|
||||
apihelper.proxy = {'https':'socks5://userproxy:password@proxy_address:port'}
|
||||
```
|
||||
|
||||
For async:
|
||||
```python
|
||||
from telebot import asyncio_helper
|
||||
|
||||
asyncio_helper.proxy = 'http://127.0.0.1:3128' #url
|
||||
```
|
||||
|
||||
|
||||
### Testing
|
||||
You can disable or change the interaction with real Telegram server by using
|
||||
```python
|
||||
@ -684,7 +709,9 @@ Result will be:
|
||||
|
||||
|
||||
## API conformance
|
||||
|
||||
* ✔ [Bot API 6.0](https://core.telegram.org/bots/api#april-16-2022)
|
||||
* ✔ [Bot API 5.7](https://core.telegram.org/bots/api#january-31-2022)
|
||||
* ✔ [Bot API 5.6](https://core.telegram.org/bots/api#december-30-2021)
|
||||
* ✔ [Bot API 5.5](https://core.telegram.org/bots/api#december-7-2021)
|
||||
* ✔ [Bot API 5.4](https://core.telegram.org/bots/api#november-5-2021)
|
||||
* ➕ [Bot API 5.3](https://core.telegram.org/bots/api#june-25-2021) - ChatMember* classes are full copies of ChatMember
|
||||
@ -716,6 +743,7 @@ Echo Bot example on AsyncTeleBot:
|
||||
# It echoes any incoming text messages.
|
||||
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
import asyncio
|
||||
bot = AsyncTeleBot('TOKEN')
|
||||
|
||||
|
||||
@ -735,7 +763,7 @@ async def echo_message(message):
|
||||
await bot.reply_to(message, message.text)
|
||||
|
||||
|
||||
bot.polling()
|
||||
asyncio.run(bot.polling())
|
||||
```
|
||||
As you can see here, keywords are await and async.
|
||||
|
||||
@ -743,10 +771,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.
|
||||
@ -755,7 +783,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
|
||||
@ -780,13 +807,25 @@ Bot instances that were idle for a long time might be rejected by the server whe
|
||||
Get help. Discuss. Chat.
|
||||
|
||||
* Join the [pyTelegramBotAPI Telegram Chat Group](https://telegram.me/joinchat/Bn4ixj84FIZVkwhk2jag6A)
|
||||
|
||||
## Telegram Channel
|
||||
|
||||
Join the [News channel](https://t.me/pyTelegramBotAPI). Here we will post releases and updates.
|
||||
|
||||
## More examples
|
||||
|
||||
* [Echo Bot](https://github.com/eternnoir/pyTelegramBotAPI/blob/master/examples/echo_bot.py)
|
||||
* [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*
|
||||
@ -834,5 +873,9 @@ Get help. Discuss. Chat.
|
||||
* [ETHGasFeeTrackerBot](https://t.me/ETHGasFeeTrackerBot) ([Source](https://github.com/DevAdvik/ETHGasFeeTrackerBot]) by *DevAdvik* - Get Live Ethereum Gas Fees in GWEI
|
||||
* [Google Sheet Bot](https://github.com/JoachimStanislaus/Tele_Sheet_bot) by [JoachimStanislaus](https://github.com/JoachimStanislaus). This bot can help you to track your expenses by uploading your bot entries to your google sheet.
|
||||
* [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
5
doc_req.txt
Normal file
@ -0,0 +1,5 @@
|
||||
-r requirements.txt
|
||||
|
||||
furo
|
||||
sphinx_copybutton
|
||||
git+https://github.com/eternnoir/pyTelegramBotAPI.git
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal 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)
|
BIN
docs/source/_static/logo.png
Normal file
BIN
docs/source/_static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
docs/source/_static/logo2.png
Normal file
BIN
docs/source/_static/logo2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
49
docs/source/async_version/index.rst
Normal file
49
docs/source/async_version/index.rst
Normal file
@ -0,0 +1,49 @@
|
||||
====================
|
||||
AsyncTeleBot
|
||||
====================
|
||||
|
||||
|
||||
.. meta::
|
||||
:description: Asynchronous pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, asynctelebot, documentation
|
||||
|
||||
|
||||
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:
|
||||
|
||||
|
||||
|
17
docs/source/calldata.rst
Normal file
17
docs/source/calldata.rst
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
=====================
|
||||
Callback data factory
|
||||
=====================
|
||||
|
||||
.. meta::
|
||||
:description: Callback data factory in pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, callbackdatafactory, guide, callbackdata, factory
|
||||
|
||||
|
||||
callback\_data file
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: telebot.callback_data
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
70
docs/source/conf.py
Normal file
70
docs/source/conf.py
Normal 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 Documentation'
|
||||
copyright = '2022, coder2020official'
|
||||
author = 'coder2020official'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '4.5.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",
|
||||
}
|
12
docs/source/formatting.rst
Normal file
12
docs/source/formatting.rst
Normal file
@ -0,0 +1,12 @@
|
||||
==================
|
||||
Formatting options
|
||||
==================
|
||||
|
||||
.. meta::
|
||||
:description: Formatting options in pyTelegramBotAPI
|
||||
:keywords: html, markdown, parse_mode, formatting, ptba, pytba, pyTelegramBotAPI
|
||||
|
||||
.. automodule:: telebot.formatting
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
69
docs/source/index.rst
Normal file
69
docs/source/index.rst
Normal file
@ -0,0 +1,69 @@
|
||||
.. 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!
|
||||
============================================
|
||||
|
||||
.. meta::
|
||||
:description: Official documentation of pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, documentation, guide
|
||||
|
||||
|
||||
=======
|
||||
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
|
||||
formatting
|
||||
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
45
docs/source/install.rst
Normal file
45
docs/source/install.rst
Normal file
@ -0,0 +1,45 @@
|
||||
==================
|
||||
Installation Guide
|
||||
==================
|
||||
|
||||
.. meta::
|
||||
:description: Installation of pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, 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
|
||||
|
||||
|
20
docs/source/quick_start.rst
Normal file
20
docs/source/quick_start.rst
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
===========
|
||||
Quick start
|
||||
===========
|
||||
|
||||
.. meta::
|
||||
:description: Quickstart guide
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, quickstart, guide
|
||||
|
||||
Synchronous TeleBot
|
||||
-------------------
|
||||
.. literalinclude:: ../../examples/echo_bot.py
|
||||
:language: python
|
||||
|
||||
Asynchronous TeleBot
|
||||
--------------------
|
||||
.. literalinclude:: ../../examples/asynchronous_telebot/echo_bot.py
|
||||
:language: python
|
||||
|
||||
|
39
docs/source/sync_version/index.rst
Normal file
39
docs/source/sync_version/index.rst
Normal file
@ -0,0 +1,39 @@
|
||||
===============
|
||||
TeleBot version
|
||||
===============
|
||||
|
||||
.. meta::
|
||||
:description: Synchronous pyTelegramBotAPI documentation
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, methods, guide, files, sync
|
||||
|
||||
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
10
docs/source/types.rst
Normal file
@ -0,0 +1,10 @@
|
||||
============
|
||||
Types of API
|
||||
============
|
||||
|
||||
|
||||
|
||||
.. automodule:: telebot.types
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
16
docs/source/util.rst
Normal file
16
docs/source/util.rst
Normal file
@ -0,0 +1,16 @@
|
||||
============
|
||||
Utils
|
||||
============
|
||||
|
||||
.. meta::
|
||||
:description: Utils in pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, utils, guide
|
||||
|
||||
|
||||
util file
|
||||
-------------------
|
||||
|
||||
.. automodule:: telebot.util
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -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:
|
||||
|
@ -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())
|
@ -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
|
@ -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())
|
@ -84,4 +84,5 @@ async def back_callback(call: types.CallbackQuery):
|
||||
|
||||
|
||||
bot.add_custom_filter(ProductsCallbackFilter())
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
@ -8,4 +8,5 @@ async def make_some(message: telebot.types.ChatJoinRequest):
|
||||
await bot.send_message(message.chat.id, 'I accepted a new user!')
|
||||
await bot.approve_chat_join_request(message.chat.id, message.from_user.id)
|
||||
|
||||
bot.polling(skip_pending=True)
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
@ -23,11 +23,12 @@ async def my_chat_m(message: types.ChatMemberUpdated):
|
||||
#content_Type_service is:
|
||||
#'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
|
||||
#'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
||||
#'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
|
||||
#'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
#'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
|
||||
#'video_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
# this handler deletes service messages
|
||||
|
||||
@bot.message_handler(content_types=util.content_type_service)
|
||||
async def delall(message: types.Message):
|
||||
await bot.delete_message(message.chat.id,message.message_id)
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling(allowed_updates=util.update_types))
|
||||
|
@ -9,4 +9,6 @@ async def answer_for_admin(message):
|
||||
|
||||
# Register filter
|
||||
bot.add_custom_filter(asyncio_filters.IsAdminFilter(bot))
|
||||
bot.polling()
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -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())
|
@ -40,4 +40,5 @@ async def bye_user(message):
|
||||
bot.add_custom_filter(MainFilter())
|
||||
bot.add_custom_filter(IsAdmin())
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -14,4 +14,5 @@ async def not_admin(message):
|
||||
|
||||
# Do not forget to register
|
||||
bot.add_custom_filter(telebot.asyncio_filters.ChatFilter())
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -19,4 +19,5 @@ async def text_filter(message):
|
||||
bot.add_custom_filter(telebot.asyncio_filters.IsReplyFilter())
|
||||
bot.add_custom_filter(telebot.asyncio_filters.ForwardFilter())
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -17,4 +17,5 @@ async def text_filter(message):
|
||||
bot.add_custom_filter(telebot.asyncio_filters.TextMatchFilter())
|
||||
bot.add_custom_filter(telebot.asyncio_filters.TextStartsFilter())
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -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,7 +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
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
@ -17,4 +17,5 @@ async def new_message(message: telebot.types.Message):
|
||||
await bot.edit_message_text(chat_id=message.chat.id, message_id=result_message.id, text='<i>Done!</i>', parse_mode='HTML')
|
||||
|
||||
|
||||
bot.polling(skip_pending=True)
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -23,4 +23,5 @@ async def echo_message(message):
|
||||
await bot.reply_to(message, message.text)
|
||||
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -24,4 +24,5 @@ async def photo_send(message: telebot.types.Message):
|
||||
|
||||
|
||||
|
||||
bot.polling(skip_pending=True)
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
52
examples/asynchronous_telebot/formatting_example.py
Normal file
52
examples/asynchronous_telebot/formatting_example.py
Normal file
@ -0,0 +1,52 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot import formatting
|
||||
|
||||
bot = AsyncTeleBot('token')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
async def start_message(message):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
# function which connects all strings
|
||||
formatting.format_text(
|
||||
formatting.mbold(message.from_user.first_name, escape=True), # pass escape=True to escape special characters
|
||||
formatting.mitalic(message.from_user.first_name, escape=True),
|
||||
formatting.munderline(message.from_user.first_name, escape=True),
|
||||
formatting.mstrikethrough(message.from_user.first_name, escape=True),
|
||||
formatting.mcode(message.from_user.first_name, escape=True),
|
||||
separator=" " # separator separates all strings
|
||||
),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# just a bold text using markdownv2
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.mbold(message.from_user.first_name, escape=True),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# html
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.format_text(
|
||||
formatting.hbold(message.from_user.first_name, escape=True),
|
||||
formatting.hitalic(message.from_user.first_name, escape=True),
|
||||
formatting.hunderline(message.from_user.first_name, escape=True),
|
||||
formatting.hstrikethrough(message.from_user.first_name, escape=True),
|
||||
formatting.hcode(message.from_user.first_name, escape=True),
|
||||
separator=" "
|
||||
),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# just a bold text in html
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.hbold(message.from_user.first_name, escape=True),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
@ -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')
|
||||
|
||||
|
||||
@ -36,4 +34,5 @@ bot.setup_middleware(SimpleMiddleware(2))
|
||||
async def start(message):
|
||||
await bot.send_message(message.chat.id, 'Hello!')
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -45,4 +45,5 @@ async def start(message, data: dict):
|
||||
await bot.send_message(message.chat.id, data['response'])
|
||||
|
||||
|
||||
bot.polling()
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -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
|
@ -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
|
@ -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 ""
|
||||
|
@ -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 "Кажется, вы перепутали язык"
|
||||
|
@ -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"
|
||||
|
@ -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())
|
@ -15,4 +15,6 @@ bot.register_message_handler(start_executor, commands=['start']) # Start command
|
||||
# bot.register_edited_message_handler(*args, **kwargs)
|
||||
# And other functions..
|
||||
|
||||
bot.polling(skip_pending=True)
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
@ -24,4 +24,6 @@ async def photos_send(message: telebot.types.Message):
|
||||
|
||||
|
||||
|
||||
bot.polling(skip_pending=True)
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
|
35
examples/asynchronous_telebot/set_command_example.py
Normal file
35
examples/asynchronous_telebot/set_command_example.py
Normal 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())
|
@ -10,4 +10,6 @@ async def send_welcome(message):
|
||||
async def echo_all(message):
|
||||
await bot.reply_to(message, message.text)
|
||||
|
||||
bot.polling(skip_pending=True)# Skip pending skips old updates
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling(skip_pending=True)) # to skip updates
|
||||
|
52
examples/asynchronous_telebot/timer_bot_async.py
Normal file
52
examples/asynchronous_telebot/timer_bot_async.py
Normal 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())
|
@ -11,4 +11,6 @@ async def update_listener(messages):
|
||||
|
||||
bot.set_update_listener(update_listener)
|
||||
|
||||
bot.polling()
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
@ -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())
|
@ -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
|
@ -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()
|
@ -23,8 +23,8 @@ def my_chat_m(message: types.ChatMemberUpdated):
|
||||
#content_Type_service is:
|
||||
#'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
|
||||
#'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
||||
#'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
|
||||
#'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
#'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
|
||||
#'video_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
# this handler deletes service messages
|
||||
|
||||
@bot.message_handler(content_types=util.content_type_service)
|
||||
|
125
examples/custom_filters/advanced_text_filter.py
Normal file
125
examples/custom_filters/advanced_text_filter.py
Normal 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()
|
@ -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)
|
51
examples/formatting_example.py
Normal file
51
examples/formatting_example.py
Normal file
@ -0,0 +1,51 @@
|
||||
from telebot import TeleBot
|
||||
from telebot import formatting
|
||||
|
||||
bot = TeleBot('TOKEN')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_message(message):
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
# function which connects all strings
|
||||
formatting.format_text(
|
||||
formatting.mbold(message.from_user.first_name, escape=True), # pass escape=True to escape special characters
|
||||
formatting.mitalic(message.from_user.first_name, escape=True),
|
||||
formatting.munderline(message.from_user.first_name, escape=True),
|
||||
formatting.mstrikethrough(message.from_user.first_name, escape=True),
|
||||
formatting.mcode(message.from_user.first_name, escape=True),
|
||||
separator=" " # separator separates all strings
|
||||
),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# just a bold text using markdownv2
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.mbold(message.from_user.first_name, escape=True),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# html
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.format_text(
|
||||
formatting.hbold(message.from_user.first_name, escape=True),
|
||||
formatting.hitalic(message.from_user.first_name, escape=True),
|
||||
formatting.hunderline(message.from_user.first_name, escape=True),
|
||||
formatting.hstrikethrough(message.from_user.first_name, escape=True),
|
||||
formatting.hcode(message.from_user.first_name, escape=True),
|
||||
separator=" "
|
||||
),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# just a bold text in html
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.hbold(message.from_user.first_name, escape=True),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
bot.infinity_polling()
|
35
examples/middleware/README.md
Normal file
35
examples/middleware/README.md
Normal 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.
|
39
examples/middleware/class_based/antiflood_middleware.py
Normal file
39
examples/middleware/class_based/antiflood_middleware.py
Normal 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()
|
32
examples/middleware/class_based/basic_example.py
Normal file
32
examples/middleware/class_based/basic_example.py
Normal 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()
|
@ -0,0 +1,121 @@
|
||||
import gettext
|
||||
import os
|
||||
import threading
|
||||
|
||||
from telebot.handler_backends import BaseMiddleware
|
||||
|
||||
try:
|
||||
from babel.support import LazyProxy
|
||||
|
||||
babel_imported = True
|
||||
except ImportError:
|
||||
babel_imported = False
|
||||
|
||||
|
||||
class I18N(BaseMiddleware):
|
||||
"""
|
||||
This middleware provides high-level tool for internationalization
|
||||
It is based on gettext util.
|
||||
"""
|
||||
|
||||
context_lang = threading.local()
|
||||
|
||||
def __init__(self, translations_path, domain_name: str):
|
||||
super().__init__()
|
||||
self.update_types = self.process_update_types()
|
||||
|
||||
self.path = translations_path
|
||||
self.domain = domain_name
|
||||
self.translations = self.find_translations()
|
||||
|
||||
@property
|
||||
def available_translations(self):
|
||||
return list(self.translations)
|
||||
|
||||
def gettext(self, text: str, lang: str = None):
|
||||
"""
|
||||
Singular translations
|
||||
"""
|
||||
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
return text
|
||||
|
||||
translator = self.translations[lang]
|
||||
return translator.gettext(text)
|
||||
|
||||
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||
"""
|
||||
Plural translations
|
||||
"""
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
if n == 1:
|
||||
return singular
|
||||
return plural
|
||||
|
||||
translator = self.translations[lang]
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
def lazy_gettext(self, text: str, lang: str = None):
|
||||
if not babel_imported:
|
||||
raise RuntimeError('babel module is not imported. Check that you installed it.')
|
||||
return LazyProxy(self.gettext, text, lang, enable_cache=False)
|
||||
|
||||
def lazy_ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||
if not babel_imported:
|
||||
raise RuntimeError('babel module is not imported. Check that you installed it.')
|
||||
return LazyProxy(self.ngettext, singular, plural, lang, n, enable_cache=False)
|
||||
|
||||
def get_user_language(self, obj):
|
||||
"""
|
||||
You need to override this method and return user language
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def process_update_types(self) -> list:
|
||||
"""
|
||||
You need to override this method and return any update types which you want to be processed
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def pre_process(self, message, data):
|
||||
"""
|
||||
context language variable will be set each time when update from 'process_update_types' comes
|
||||
value is the result of 'get_user_language' method
|
||||
"""
|
||||
|
||||
self.context_lang.language = self.get_user_language(obj=message)
|
||||
|
||||
def post_process(self, message, data, exception):
|
||||
pass
|
||||
|
||||
def find_translations(self):
|
||||
"""
|
||||
Looks for translations with passed 'domain' in passed 'path'
|
||||
"""
|
||||
if not os.path.exists(self.path):
|
||||
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
|
||||
|
||||
result = {}
|
||||
|
||||
for name in os.listdir(self.path):
|
||||
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
|
||||
|
||||
if not os.path.isdir(translations_path):
|
||||
continue
|
||||
|
||||
po_file = os.path.join(translations_path, self.domain + '.po')
|
||||
mo_file = po_file[:-2] + 'mo'
|
||||
|
||||
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
|
||||
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
|
||||
|
||||
with open(mo_file, 'rb') as file:
|
||||
result[name] = gettext.GNUTranslations(file)
|
||||
|
||||
return result
|
34
examples/middleware/class_based/i18n_middleware/keyboards.py
Normal file
34
examples/middleware/class_based/i18n_middleware/keyboards.py
Normal 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
|
@ -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 ""
|
||||
|
@ -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 "Кажется, вы перепутали язык"
|
||||
|
@ -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"
|
||||
|
211
examples/middleware/class_based/i18n_middleware/main.py
Normal file
211
examples/middleware/class_based/i18n_middleware/main.py
Normal file
@ -0,0 +1,211 @@
|
||||
"""
|
||||
In this example you will learn how to adapt your bot to different languages
|
||||
Using built-in middleware I18N.
|
||||
|
||||
You need to install babel package 'https://pypi.org/project/Babel/'
|
||||
Babel provides a command-line interface for working with message catalogs
|
||||
After installing babel package you have a script called 'pybabel'
|
||||
Too see all the commands open terminal and type 'pybabel --help'
|
||||
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
|
||||
|
||||
Create a directory 'locales' where our translations will be stored
|
||||
|
||||
First we need to extract texts:
|
||||
pybabel extract -o locales/{domain_name}.pot --input-dirs .
|
||||
{domain_name}.pot - is the file where all translations are saved
|
||||
The name of this file should be the same as domain which you pass to I18N class
|
||||
In this example domain_name will be 'messages'
|
||||
|
||||
For gettext (singular texts) we use '_' alias and it works perfect
|
||||
You may also you some alias for ngettext (plural texts) but you can face with a problem that
|
||||
your plural texts are not being extracted
|
||||
That is because by default 'pybabel extract' recognizes the following keywords:
|
||||
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
|
||||
To add your own keyword you can use '-k' flag
|
||||
In this example for 'ngettext' i will assign double underscore alias '__'
|
||||
|
||||
Full command with pluralization support will look so:
|
||||
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||
|
||||
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l en
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l ru
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
|
||||
|
||||
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
|
||||
After you translated all the texts you need to compile .po files:
|
||||
pybabel compile -d locales
|
||||
|
||||
When you delete/update your texts you also need to update them in .po files:
|
||||
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||
pybabel update -i locales/{domain_name}.pot -d locales
|
||||
- translate
|
||||
pybabel compile -d locales
|
||||
|
||||
If you have any exceptions check:
|
||||
- you have installed babel
|
||||
- translations are ready, so you just compiled it
|
||||
- in the commands above you replaced {domain_name} to messages
|
||||
- you are writing commands from correct path in terminal
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Union
|
||||
|
||||
import keyboards
|
||||
from i18n_base_midddleware import I18N
|
||||
from telebot import TeleBot
|
||||
from telebot import types, StateMemoryStorage
|
||||
from telebot.custom_filters import TextMatchFilter, TextFilter
|
||||
|
||||
class I18NMiddleware(I18N):
|
||||
|
||||
def process_update_types(self) -> list:
|
||||
"""
|
||||
Here you need to return a list of update types which you want to be processed
|
||||
"""
|
||||
return ['message', 'callback_query']
|
||||
|
||||
def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]):
|
||||
"""
|
||||
This method is called when new update comes (only updates which you return in 'process_update_types' method)
|
||||
Returned language will be used in 'pre_process' method of parent class
|
||||
Returned language will be set to context language variable.
|
||||
If you need to get translation with user's actual language you don't have to pass it manually
|
||||
It will be automatically passed from context language value.
|
||||
However if you need some other language you can always pass it.
|
||||
"""
|
||||
|
||||
user_id = obj.from_user.id
|
||||
|
||||
if user_id not in users_lang:
|
||||
users_lang[user_id] = 'en'
|
||||
|
||||
return users_lang[user_id]
|
||||
|
||||
|
||||
storage = StateMemoryStorage()
|
||||
bot = TeleBot("", state_storage=storage, use_class_middlewares=True)
|
||||
|
||||
i18n = I18NMiddleware(translations_path='locales', domain_name='messages')
|
||||
_ = i18n.gettext # for singular translations
|
||||
__ = i18n.ngettext # for plural translations
|
||||
|
||||
# These are example storages, do not use it in a production development
|
||||
users_lang = {}
|
||||
users_clicks = {}
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_handler(message: types.Message):
|
||||
text = _("Hello, {user_fist_name}!\n"
|
||||
"This is the example of multilanguage bot.\n"
|
||||
"Available commands:\n\n"
|
||||
"/lang - change your language\n"
|
||||
"/plural - pluralization example\n"
|
||||
"/menu - text menu example")
|
||||
|
||||
# remember don't use f string for interpolation, use .format method instead
|
||||
text = text.format(user_fist_name=message.from_user.first_name)
|
||||
bot.send_message(message.from_user.id, text)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['lang'])
|
||||
def change_language_handler(message: types.Message):
|
||||
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
|
||||
reply_markup=keyboards.languages_keyboard())
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn']))
|
||||
def language_handler(call: types.CallbackQuery):
|
||||
lang = call.data
|
||||
users_lang[call.from_user.id] = lang
|
||||
|
||||
# When you changed user language, you have to pass it manually beacause it is not changed in context
|
||||
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['plural'])
|
||||
def pluralization_handler(message: types.Message):
|
||||
if not users_clicks.get(message.from_user.id):
|
||||
users_clicks[message.from_user.id] = 0
|
||||
clicks = users_clicks[message.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
|
||||
def click_handler(call: types.CallbackQuery):
|
||||
if not users_clicks.get(call.from_user.id):
|
||||
users_clicks[call.from_user.id] = 1
|
||||
else:
|
||||
users_clicks[call.from_user.id] += 1
|
||||
|
||||
clicks = users_clicks[call.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
|
||||
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||
reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.message_handler(commands=['menu'])
|
||||
def menu_handler(message: types.Message):
|
||||
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_))
|
||||
|
||||
|
||||
# For lazy tranlations
|
||||
# lazy gettext is used when you don't know user's locale
|
||||
# It can be used for example to handle text buttons in multilanguage bot
|
||||
# The actual translation will be delayed until update comes and context language is set
|
||||
l_ = i18n.lazy_gettext
|
||||
|
||||
|
||||
# Handlers below will handle text according to user's language
|
||||
@bot.message_handler(text=l_("My user id"))
|
||||
def return_user_id(message: types.Message):
|
||||
bot.send_message(message.chat.id, str(message.from_user.id))
|
||||
|
||||
|
||||
@bot.message_handler(text=l_("My user name"))
|
||||
def return_user_id(message: types.Message):
|
||||
username = message.from_user.username
|
||||
if not username:
|
||||
username = '-'
|
||||
bot.send_message(message.chat.id, username)
|
||||
|
||||
|
||||
# You can make it case-insensitive
|
||||
@bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True))
|
||||
def return_user_id(message: types.Message):
|
||||
bot.send_message(message.chat.id, message.from_user.first_name)
|
||||
|
||||
|
||||
all_menu_texts = []
|
||||
for language in i18n.available_translations:
|
||||
for menu_text in ("My user id", "My user name", "My first name"):
|
||||
all_menu_texts.append(_(menu_text, language))
|
||||
|
||||
|
||||
# When user confused language. (handles all menu buttons texts)
|
||||
@bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True))
|
||||
def missed_message(message: types.Message):
|
||||
bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.setup_middleware(i18n)
|
||||
bot.add_custom_filter(TextMatchFilter())
|
||||
asyncio.run(bot.infinity_polling())
|
@ -0,0 +1,77 @@
|
||||
import gettext
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
class I18N:
|
||||
"""
|
||||
This class provides high-level tool for internationalization
|
||||
It is based on gettext util.
|
||||
"""
|
||||
|
||||
context_lang = threading.local()
|
||||
|
||||
def __init__(self, translations_path, domain_name: str):
|
||||
self.path = translations_path
|
||||
self.domain = domain_name
|
||||
self.translations = self.find_translations()
|
||||
|
||||
@property
|
||||
def available_translations(self):
|
||||
return list(self.translations)
|
||||
|
||||
def gettext(self, text: str, lang: str = None):
|
||||
"""
|
||||
Singular translations
|
||||
"""
|
||||
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
return text
|
||||
|
||||
translator = self.translations[lang]
|
||||
return translator.gettext(text)
|
||||
|
||||
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||
"""
|
||||
Plural translations
|
||||
"""
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
if n == 1:
|
||||
return singular
|
||||
return plural
|
||||
|
||||
translator = self.translations[lang]
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
|
||||
def 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
|
23
examples/middleware/function_based/i18n_gettext/keyboards.py
Normal file
23
examples/middleware/function_based/i18n_gettext/keyboards.py
Normal 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(_):
|
||||
return InlineKeyboardMarkup(
|
||||
keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text=_("click"), callback_data='click'),
|
||||
]
|
||||
]
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
# 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 ""
|
@ -0,0 +1,59 @@
|
||||
# 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"
|
@ -0,0 +1,57 @@
|
||||
# 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"
|
134
examples/middleware/function_based/i18n_gettext/main.py
Normal file
134
examples/middleware/function_based/i18n_gettext/main.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""
|
||||
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 telebot import TeleBot, types, custom_filters
|
||||
from telebot import apihelper
|
||||
from telebot.storage.memory_storage import StateMemoryStorage
|
||||
|
||||
import keyboards
|
||||
from i18n_class import I18N
|
||||
|
||||
apihelper.ENABLE_MIDDLEWARE = True
|
||||
storage = StateMemoryStorage()
|
||||
# IMPORTANT! This example works only if polling is non-threaded.
|
||||
bot = TeleBot("", state_storage=storage, threaded=False)
|
||||
|
||||
i18n = I18N(translations_path='locales', domain_name='messages')
|
||||
_ = i18n.gettext # for singular translations
|
||||
__ = i18n.ngettext # for plural translations
|
||||
|
||||
# These are example storages, do not use it in a production development
|
||||
users_lang = {}
|
||||
users_clicks = {}
|
||||
|
||||
|
||||
@bot.middleware_handler(update_types=['message', 'callback_query'])
|
||||
def set_contex_language(bot_instance, message):
|
||||
i18n.context_lang.language = users_lang.get(message.from_user.id, 'en')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
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")
|
||||
|
||||
# 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
|
||||
|
||||
# When you change user's language, pass language explicitly coz it's not changed in context
|
||||
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||
bot.delete_state(call.from_user.id)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['plural'])
|
||||
def pluralization_handler(message: types.Message):
|
||||
if not users_clicks.get(message.from_user.id):
|
||||
users_clicks[message.from_user.id] = 0
|
||||
clicks = users_clicks[message.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks,
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
|
||||
def click_handler(call: types.CallbackQuery):
|
||||
if not users_clicks.get(call.from_user.id):
|
||||
users_clicks[call.from_user.id] = 1
|
||||
else:
|
||||
users_clicks[call.from_user.id] += 1
|
||||
|
||||
clicks = users_clicks[call.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||
reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.add_custom_filter(custom_filters.TextMatchFilter())
|
||||
bot.infinity_polling()
|
@ -38,21 +38,20 @@ def command_pay(message):
|
||||
"Real cards won't work with me, no money will be debited from your account."
|
||||
" Use this test card number to pay for your Time Machine: `4242 4242 4242 4242`"
|
||||
"\n\nThis is your demo invoice:", parse_mode='Markdown')
|
||||
bot.send_invoice(message.chat.id, title='Working Time Machine',
|
||||
description='Want to visit your great-great-great-grandparents?'
|
||||
' Make a fortune at the races?'
|
||||
' Shake hands with Hammurabi and take a stroll in the Hanging Gardens?'
|
||||
' Order our Working Time Machine today!',
|
||||
provider_token=provider_token,
|
||||
currency='usd',
|
||||
bot.send_invoice(
|
||||
message.chat.id, #chat_id
|
||||
'Working Time Machine', #title
|
||||
' Want to visit your great-great-great-grandparents? Make a fortune at the races? Shake hands with Hammurabi and take a stroll in the Hanging Gardens? Order our Working Time Machine today!', #description
|
||||
'HAPPY FRIDAYS COUPON', #invoice_payload
|
||||
provider_token, #provider_token
|
||||
'usd', #currency
|
||||
prices, #prices
|
||||
photo_url='http://erkelzaar.tsudao.com/models/perrotta/TIME_MACHINE.jpg',
|
||||
photo_height=512, # !=0/None or picture won't be shown
|
||||
photo_width=512,
|
||||
photo_size=512,
|
||||
is_flexible=False, # True If you need to set up Shipping Fee
|
||||
prices=prices,
|
||||
start_parameter='time-machine-example',
|
||||
invoice_payload='HAPPY FRIDAYS COUPON')
|
||||
start_parameter='time-machine-example')
|
||||
|
||||
|
||||
@bot.shipping_query_handler(func=lambda query: True)
|
||||
|
28
examples/set_command_example.py
Normal file
28
examples/set_command_example.py
Normal 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
42
examples/timer_bot.py
Normal 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)
|
@ -50,5 +50,12 @@ There are 5 examples in this directory using different libraries:
|
||||
* **Cons:**
|
||||
* Twisted is low-level, which may be good or bad depending on use case
|
||||
* Considerable learning curve - reading docs is a must.
|
||||
* **FastAPI(0.70.1):** *webhook_fastapi_echo_bot.py*
|
||||
* **Pros:**
|
||||
* Can be written for both sync and async
|
||||
* Good documentation
|
||||
* **Cons:**
|
||||
* Requires python 3.6+
|
||||
|
||||
|
||||
*Latest update of this document: 2020-12-17*
|
||||
*Latest update of this document: 01-03-2022
|
||||
|
79
examples/webhook_examples/webhook_fastapi_echo_bot.py
Normal file
79
examples/webhook_examples/webhook_fastapi_echo_bot.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This is a simple echo bot using decorators and webhook with fastapi
|
||||
# It echoes any incoming text messages and does not use the polling method.
|
||||
|
||||
import logging
|
||||
import fastapi
|
||||
import telebot
|
||||
|
||||
API_TOKEN = 'TOKEN'
|
||||
|
||||
WEBHOOK_HOST = '<ip/domain>'
|
||||
WEBHOOK_PORT = 8443 # 443, 80, 88 or 8443 (port need to be 'open')
|
||||
WEBHOOK_LISTEN = '0.0.0.0' # In some VPS you may need to put here the IP addr
|
||||
|
||||
WEBHOOK_SSL_CERT = './webhook_cert.pem' # Path to the ssl certificate
|
||||
WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key
|
||||
|
||||
# Quick'n'dirty SSL certificate generation:
|
||||
#
|
||||
# openssl genrsa -out webhook_pkey.pem 2048
|
||||
# openssl req -new -x509 -days 3650 -key webhook_pkey.pem -out webhook_cert.pem
|
||||
#
|
||||
# When asked for "Common Name (e.g. server FQDN or YOUR name)" you should reply
|
||||
# with the same value in you put in WEBHOOK_HOST
|
||||
|
||||
WEBHOOK_URL_BASE = "https://{}:{}".format(WEBHOOK_HOST, WEBHOOK_PORT)
|
||||
WEBHOOK_URL_PATH = "/{}/".format(API_TOKEN)
|
||||
|
||||
logger = telebot.logger
|
||||
telebot.logger.setLevel(logging.INFO)
|
||||
|
||||
bot = telebot.TeleBot(API_TOKEN)
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
|
||||
|
||||
# Process webhook calls
|
||||
@app.post(f'/{API_TOKEN}/')
|
||||
def process_webhook(update: dict):
|
||||
if update:
|
||||
update = telebot.types.Update.de_json(update)
|
||||
bot.process_new_updates([update])
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
|
||||
# Handle '/start' and '/help'
|
||||
@bot.message_handler(commands=['help', 'start'])
|
||||
def send_welcome(message):
|
||||
bot.reply_to(message,
|
||||
("Hi there, I am EchoBot.\n"
|
||||
"I am here to echo your kind words back to you."))
|
||||
|
||||
|
||||
# Handle all other messages
|
||||
@bot.message_handler(func=lambda message: True, content_types=['text'])
|
||||
def echo_message(message):
|
||||
bot.reply_to(message, message.text)
|
||||
|
||||
|
||||
# Remove webhook, it fails sometimes the set if there is a previous webhook
|
||||
bot.remove_webhook()
|
||||
|
||||
# Set webhook
|
||||
bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH,
|
||||
certificate=open(WEBHOOK_SSL_CERT, 'r'))
|
||||
|
||||
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=WEBHOOK_LISTEN,
|
||||
port=WEBHOOK_PORT,
|
||||
ssl_certfile=WEBHOOK_SSL_CERT,
|
||||
ssl_keyfile=WEBHOOK_SSL_PRIV
|
||||
)
|
@ -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={
|
||||
|
@ -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
|
||||
|
10
setup.py
10
setup.py
@ -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,19 +19,21 @@ 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'],
|
||||
extras_require={
|
||||
'json': 'ujson',
|
||||
'PIL': 'Pillow',
|
||||
'redis': 'redis>=3.4.1'
|
||||
'redis': 'redis>=3.4.1',
|
||||
'aiohttp': 'aiohttp',
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
]
|
||||
],
|
||||
|
||||
)
|
||||
|
1550
telebot/__init__.py
1550
telebot/__init__.py
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,6 @@ CUSTOM_REQUEST_SENDER = None
|
||||
ENABLE_MIDDLEWARE = False
|
||||
|
||||
|
||||
|
||||
def _get_req_session(reset=False):
|
||||
if SESSION_TIME_TO_LIVE:
|
||||
# If session TTL is set - check time passed
|
||||
@ -94,20 +93,14 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
||||
if 'timeout' in params:
|
||||
read_timeout = params.pop('timeout')
|
||||
connect_timeout = read_timeout
|
||||
# if 'connect-timeout' in params:
|
||||
# connect_timeout = params.pop('connect-timeout') + 10
|
||||
if 'long_polling_timeout' in params:
|
||||
# For getUpdates: it's the only function with timeout parameter on the BOT API side
|
||||
# For getUpdates. It's the only function with timeout parameter on the BOT API side
|
||||
long_polling_timeout = params.pop('long_polling_timeout')
|
||||
params['timeout'] = long_polling_timeout
|
||||
# Long polling hangs for a given time. Read timeout should be greater that long_polling_timeout
|
||||
read_timeout = max(long_polling_timeout + 5, read_timeout)
|
||||
# Lets stop suppose that user is stupid and assume that he knows what he do...
|
||||
# read_timeout = read_timeout + 10
|
||||
# connect_timeout = connect_timeout + 10
|
||||
|
||||
params = params or None # Set params to None if empty
|
||||
|
||||
result = None
|
||||
if RETRY_ON_ERROR and RETRY_ENGINE == 1:
|
||||
got_result = False
|
||||
@ -134,6 +127,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
||||
timeout=(connect_timeout, read_timeout), proxies=proxy)
|
||||
elif RETRY_ON_ERROR and RETRY_ENGINE == 2:
|
||||
http = _get_req_session()
|
||||
# noinspection PyUnresolvedReferences
|
||||
retry_strategy = requests.packages.urllib3.util.retry.Retry(
|
||||
total=MAX_RETRIES,
|
||||
)
|
||||
@ -144,6 +138,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
||||
method, request_url, params=params, files=files,
|
||||
timeout=(connect_timeout, read_timeout), proxies=proxy)
|
||||
elif CUSTOM_REQUEST_SENDER:
|
||||
# noinspection PyCallingNonCallable
|
||||
result = CUSTOM_REQUEST_SENDER(
|
||||
method, request_url, params=params, files=files,
|
||||
timeout=(connect_timeout, read_timeout), proxies=proxy)
|
||||
@ -232,7 +227,7 @@ def send_message(
|
||||
token, chat_id, text,
|
||||
disable_web_page_preview=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None,
|
||||
entities=None, allow_sending_without_reply=None):
|
||||
entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
"""
|
||||
Use this method to send text messages. On success, the sent Message is returned.
|
||||
:param token:
|
||||
@ -246,6 +241,7 @@ def send_message(
|
||||
:param timeout:
|
||||
:param entities:
|
||||
:param allow_sending_without_reply:
|
||||
:param protect_content:
|
||||
:return:
|
||||
"""
|
||||
method_url = r'sendMessage'
|
||||
@ -266,6 +262,8 @@ def send_message(
|
||||
payload['entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
@ -390,19 +388,21 @@ def get_chat_member(token, chat_id, user_id):
|
||||
|
||||
def forward_message(
|
||||
token, chat_id, from_chat_id, message_id,
|
||||
disable_notification=None, timeout=None):
|
||||
disable_notification=None, timeout=None, protect_content=None):
|
||||
method_url = r'forwardMessage'
|
||||
payload = {'chat_id': chat_id, 'from_chat_id': from_chat_id, 'message_id': message_id}
|
||||
if disable_notification is not None:
|
||||
payload['disable_notification'] = disable_notification
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
def copy_message(token, chat_id, from_chat_id, message_id, caption=None, parse_mode=None, caption_entities=None,
|
||||
disable_notification=None, reply_to_message_id=None, allow_sending_without_reply=None,
|
||||
reply_markup=None, timeout=None):
|
||||
reply_markup=None, timeout=None, protect_content=None):
|
||||
method_url = r'copyMessage'
|
||||
payload = {'chat_id': chat_id, 'from_chat_id': from_chat_id, 'message_id': message_id}
|
||||
if caption is not None:
|
||||
@ -421,13 +421,15 @@ def copy_message(token, chat_id, from_chat_id, message_id, caption=None, parse_m
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
def send_dice(
|
||||
token, chat_id,
|
||||
emoji=None, disable_notification=None, reply_to_message_id=None,
|
||||
reply_markup=None, timeout=None, allow_sending_without_reply=None):
|
||||
reply_markup=None, timeout=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendDice'
|
||||
payload = {'chat_id': chat_id}
|
||||
if emoji:
|
||||
@ -442,6 +444,8 @@ def send_dice(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -449,7 +453,7 @@ def send_photo(
|
||||
token, chat_id, photo,
|
||||
caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None,
|
||||
caption_entities=None, allow_sending_without_reply=None):
|
||||
caption_entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendPhoto'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -475,13 +479,15 @@ def send_photo(
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_media_group(
|
||||
token, chat_id, media,
|
||||
disable_notification=None, reply_to_message_id=None,
|
||||
timeout=None, allow_sending_without_reply=None):
|
||||
timeout=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendMediaGroup'
|
||||
media_json, files = convert_input_media_array(media)
|
||||
payload = {'chat_id': chat_id, 'media': media_json}
|
||||
@ -493,6 +499,8 @@ def send_media_group(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(
|
||||
token, method_url, params=payload,
|
||||
method='post' if files else 'get',
|
||||
@ -504,7 +512,7 @@ def send_location(
|
||||
live_period=None, reply_to_message_id=None,
|
||||
reply_markup=None, disable_notification=None,
|
||||
timeout=None, horizontal_accuracy=None, heading=None,
|
||||
proximity_alert_radius=None, allow_sending_without_reply=None):
|
||||
proximity_alert_radius=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendLocation'
|
||||
payload = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude}
|
||||
if live_period:
|
||||
@ -525,6 +533,8 @@ def send_location(
|
||||
payload['disable_notification'] = disable_notification
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -576,7 +586,7 @@ def send_venue(
|
||||
foursquare_id=None, foursquare_type=None, disable_notification=None,
|
||||
reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None, google_place_id=None,
|
||||
google_place_type=None):
|
||||
google_place_type=None, protect_content=None):
|
||||
method_url = r'sendVenue'
|
||||
payload = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude, 'title': title, 'address': address}
|
||||
if foursquare_id:
|
||||
@ -597,13 +607,15 @@ def send_venue(
|
||||
payload['google_place_id'] = google_place_id
|
||||
if google_place_type:
|
||||
payload['google_place_type'] = google_place_type
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
def send_contact(
|
||||
token, chat_id, phone_number, first_name, last_name=None, vcard=None,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendContact'
|
||||
payload = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name}
|
||||
if last_name:
|
||||
@ -620,6 +632,9 @@ def send_contact(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -633,7 +648,7 @@ def send_chat_action(token, chat_id, action, timeout=None):
|
||||
|
||||
def send_video(token, chat_id, data, duration=None, caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, supports_streaming=None, disable_notification=None, timeout=None,
|
||||
thumb=None, width=None, height=None, caption_entities=None, allow_sending_without_reply=None):
|
||||
thumb=None, width=None, height=None, caption_entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendVideo'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -673,13 +688,15 @@ def send_video(token, chat_id, data, duration=None, caption=None, reply_to_messa
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_animation(
|
||||
token, chat_id, data, duration=None, caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None, thumb=None, caption_entities=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None, width=None, height=None):
|
||||
method_url = r'sendAnimation'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -713,12 +730,18 @@ def send_animation(
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
if width:
|
||||
payload['width'] = width
|
||||
if height:
|
||||
payload['height'] = height
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_voice(token, chat_id, voice, caption=None, duration=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None, caption_entities=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendVoice'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -744,11 +767,13 @@ def send_voice(token, chat_id, voice, caption=None, duration=None, reply_to_mess
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_video_note(token, chat_id, data, duration=None, length=None, reply_to_message_id=None, reply_markup=None,
|
||||
disable_notification=None, timeout=None, thumb=None, allow_sending_without_reply=None):
|
||||
disable_notification=None, timeout=None, thumb=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendVideoNote'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -780,12 +805,14 @@ def send_video_note(token, chat_id, data, duration=None, length=None, reply_to_m
|
||||
payload['thumb'] = thumb
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_audio(token, chat_id, audio, caption=None, duration=None, performer=None, title=None, reply_to_message_id=None,
|
||||
reply_markup=None, parse_mode=None, disable_notification=None, timeout=None, thumb=None,
|
||||
caption_entities=None, allow_sending_without_reply=None):
|
||||
caption_entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendAudio'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -823,12 +850,15 @@ def send_audio(token, chat_id, audio, caption=None, duration=None, performer=Non
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
def send_data(token, chat_id, data, data_type, reply_to_message_id=None, reply_markup=None, parse_mode=None,
|
||||
disable_notification=None, timeout=None, caption=None, thumb=None, caption_entities=None,
|
||||
allow_sending_without_reply=None, disable_content_type_detection=None, visible_file_name=None):
|
||||
allow_sending_without_reply=None, disable_content_type_detection=None, visible_file_name=None,
|
||||
protect_content = None):
|
||||
method_url = get_method_by_type(data_type)
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -863,6 +893,8 @@ def send_data(token, chat_id, data, data_type, reply_to_message_id=None, reply_m
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
if method_url == 'sendDocument' and disable_content_type_detection is not None:
|
||||
payload['disable_content_type_detection'] = disable_content_type_detection
|
||||
return _make_request(token, method_url, params=payload, files=files, method='post')
|
||||
@ -933,7 +965,7 @@ def promote_chat_member(
|
||||
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
|
||||
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
|
||||
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
|
||||
method_url = 'promoteChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
if can_change_info is not None:
|
||||
@ -956,8 +988,8 @@ def promote_chat_member(
|
||||
payload['is_anonymous'] = is_anonymous
|
||||
if can_manage_chat is not None:
|
||||
payload['can_manage_chat'] = can_manage_chat
|
||||
if can_manage_voice_chats is not None:
|
||||
payload['can_manage_voice_chats'] = can_manage_voice_chats
|
||||
if can_manage_video_chats is not None:
|
||||
payload['can_manage_video_chats'] = can_manage_video_chats
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
@ -1101,6 +1133,40 @@ def get_my_commands(token, scope=None, language_code=None):
|
||||
payload['language_code'] = language_code
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
def set_chat_menu_button(token, chat_id=None, menu_button=None):
|
||||
method_url = r'setChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
if menu_button:
|
||||
payload['menu_button'] = menu_button.to_json()
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
def get_chat_menu_button(token, chat_id=None):
|
||||
method_url = r'getChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def set_my_default_administrator_rights(token, rights=None, for_channels=None):
|
||||
method_url = r'setMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if rights:
|
||||
payload['rights'] = rights.to_json()
|
||||
if for_channels is not None:
|
||||
payload['for_channels'] = for_channels
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def get_my_default_administrator_rights(token, for_channels=None):
|
||||
method_url = r'getMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if for_channels:
|
||||
payload['for_channels'] = for_channels
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def set_my_commands(token, commands, scope=None, language_code=None):
|
||||
method_url = r'setMyCommands'
|
||||
@ -1236,7 +1302,7 @@ def delete_message(token, chat_id, message_id, timeout=None):
|
||||
def send_game(
|
||||
token, chat_id, game_short_name,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendGame'
|
||||
payload = {'chat_id': chat_id, 'game_short_name': game_short_name}
|
||||
if disable_notification is not None:
|
||||
@ -1249,6 +1315,8 @@ def send_game(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -1313,7 +1381,8 @@ def send_invoice(
|
||||
need_name=None, need_phone_number=None, need_email=None, need_shipping_address=None,
|
||||
send_phone_number_to_provider = None, send_email_to_provider = None, is_flexible=None,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, provider_data=None,
|
||||
timeout=None, allow_sending_without_reply=None, max_tip_amount=None, suggested_tip_amounts=None):
|
||||
timeout=None, allow_sending_without_reply=None, max_tip_amount=None, suggested_tip_amounts=None,
|
||||
protect_content=None):
|
||||
"""
|
||||
Use this method to send invoices. On success, the sent Message is returned.
|
||||
:param token: Bot's token (you don't need to fill this)
|
||||
@ -1345,6 +1414,7 @@ def send_invoice(
|
||||
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
|
||||
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest units of the currency.
|
||||
At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount.
|
||||
:param protect_content:
|
||||
:return:
|
||||
"""
|
||||
method_url = r'sendInvoice'
|
||||
@ -1391,6 +1461,8 @@ def send_invoice(
|
||||
payload['max_tip_amount'] = max_tip_amount
|
||||
if suggested_tip_amounts is not None:
|
||||
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -1488,11 +1560,16 @@ 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
|
||||
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}
|
||||
@ -1502,14 +1579,21 @@ 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
|
||||
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}
|
||||
@ -1532,6 +1616,12 @@ def delete_sticker_from_set(token, sticker):
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
|
||||
method_url = 'answerWebAppQuery'
|
||||
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def send_poll(
|
||||
token, chat_id,
|
||||
@ -1539,7 +1629,7 @@ def send_poll(
|
||||
is_anonymous = None, type = None, allows_multiple_answers = None, correct_option_id = None,
|
||||
explanation = None, explanation_parse_mode=None, open_period = None, close_date = None, is_closed = None,
|
||||
disable_notification=False, reply_to_message_id=None, allow_sending_without_reply=None,
|
||||
reply_markup=None, timeout=None, explanation_entities=None):
|
||||
reply_markup=None, timeout=None, explanation_entities=None, protect_content=None):
|
||||
method_url = r'sendPoll'
|
||||
payload = {
|
||||
'chat_id': str(chat_id),
|
||||
@ -1581,6 +1671,8 @@ def send_poll(
|
||||
if explanation_entities:
|
||||
payload['explanation_entities'] = json.dumps(
|
||||
types.MessageEntity.to_list_of_dicts(explanation_entities))
|
||||
if protect_content:
|
||||
payload['protect_content'] = protect_content
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -1674,7 +1766,8 @@ class ApiException(Exception):
|
||||
super(ApiException, self).__init__("A request to the Telegram API was unsuccessful. {0}".format(msg))
|
||||
self.function_name = function_name
|
||||
self.result = result
|
||||
|
||||
|
||||
|
||||
class ApiHTTPException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a call to the
|
||||
@ -1686,7 +1779,8 @@ class ApiHTTPException(ApiException):
|
||||
.format(result.status_code, result.reason, result.text.encode('utf8')),
|
||||
function_name,
|
||||
result)
|
||||
|
||||
|
||||
|
||||
class ApiInvalidJSONException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a call to the
|
||||
@ -1698,7 +1792,8 @@ class ApiInvalidJSONException(ApiException):
|
||||
.format(result.text.encode('utf8')),
|
||||
function_name,
|
||||
result)
|
||||
|
||||
|
||||
|
||||
class ApiTelegramException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a Telegram API returns error code.
|
||||
@ -1712,4 +1807,3 @@ class ApiTelegramException(ApiException):
|
||||
self.result_json = result_json
|
||||
self.error_code = result_json['error_code']
|
||||
self.description = result_json['description']
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,21 @@
|
||||
from abc import ABC
|
||||
from typing import Optional, Union
|
||||
from telebot.asyncio_handler_backends import State
|
||||
|
||||
from telebot import types
|
||||
|
||||
|
||||
class SimpleCustomFilter(ABC):
|
||||
"""
|
||||
Simple Custom Filter base class.
|
||||
Create child class with check() method.
|
||||
Accepts only message, returns bool value, that is compared with given in handler.
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
async def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
@ -21,8 +30,12 @@ class AdvancedCustomFilter(ABC):
|
||||
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
|
||||
message: Message class
|
||||
text: Filter value given in handler
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
async def check(self, message, text):
|
||||
"""
|
||||
Perform a check.
|
||||
@ -30,6 +43,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/asynchronous_telebot/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 +144,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 +165,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 +185,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 +199,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 +218,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 +234,6 @@ class IsReplyFilter(SimpleCustomFilter):
|
||||
return message.reply_to_message is not None
|
||||
|
||||
|
||||
|
||||
class LanguageFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check users language_code.
|
||||
@ -127,8 +246,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 +269,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 +277,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):
|
||||
"""
|
||||
|
@ -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:
|
||||
new_data = 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
|
@ -1,8 +1,6 @@
|
||||
import asyncio # for future uses
|
||||
from time import time
|
||||
import aiohttp
|
||||
from telebot import types
|
||||
import json
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
@ -13,17 +11,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
|
||||
@ -34,26 +23,64 @@ CONNECT_TIMEOUT = 15
|
||||
READ_TIMEOUT = 30
|
||||
|
||||
LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates)
|
||||
REQUEST_TIMEOUT = 10
|
||||
MAX_RETRIES = 3
|
||||
|
||||
logger = telebot.logger
|
||||
REQUEST_LIMIT = 50
|
||||
|
||||
RETRY_ON_ERROR = False
|
||||
RETRY_TIMEOUT = 2
|
||||
MAX_RETRIES = 15
|
||||
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 = compose_data(params, files)
|
||||
async with await session_manager._get_new_session() as session:
|
||||
async with session.request(method=method, url=API_URL.format(token, url), data=params, timeout=request_timeout) as response:
|
||||
logger.debug("Request: method={0} url={1} params={2} files={3} request_timeout={4}".format(method, url, params, files, request_timeout).replace(token, token.split(':')[0] + ":{TOKEN}"))
|
||||
json_result = await _check_result(url, response)
|
||||
if json_result:
|
||||
return json_result['result']
|
||||
params = prepare_data(params, files)
|
||||
if request_timeout is None:
|
||||
request_timeout = REQUEST_TIMEOUT
|
||||
timeout = aiohttp.ClientTimeout(total=request_timeout)
|
||||
got_result = False
|
||||
current_try=0
|
||||
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, proxy=proxy) 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, resp)
|
||||
if json_result:
|
||||
got_result = True
|
||||
return json_result['result']
|
||||
except (ApiTelegramException,ApiInvalidJSONException, ApiHTTPException) as e:
|
||||
raise e
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error('Aiohttp ClientError: {0}'.format(e.__class__.__name__))
|
||||
except Exception as e:
|
||||
logger.error(f'Unknown error: {e.__class__.__name__}')
|
||||
if not got_result:
|
||||
raise RequestTimeout("Request timeout. Request: method={0} url={1} params={2} files={3} request_timeout={4}".format(method, url, params, files, request_timeout, current_try))
|
||||
|
||||
|
||||
|
||||
def guess_filename(obj):
|
||||
|
||||
def prepare_file(obj):
|
||||
"""
|
||||
Get file name from object
|
||||
returns os.path.basename for a given file
|
||||
|
||||
:param obj:
|
||||
:return:
|
||||
@ -63,9 +90,9 @@ def guess_filename(obj):
|
||||
return os.path.basename(name)
|
||||
|
||||
|
||||
def compose_data(params=None, files=None):
|
||||
def prepare_data(params=None, files=None):
|
||||
"""
|
||||
Prepare request data
|
||||
prepare data for request.
|
||||
|
||||
:param params:
|
||||
:param files:
|
||||
@ -85,7 +112,7 @@ def compose_data(params=None, files=None):
|
||||
else:
|
||||
raise ValueError('Tuple must have exactly 2 elements: filename, fileobj')
|
||||
else:
|
||||
filename, fileobj = guess_filename(f) or key, f
|
||||
filename, fileobj = prepare_file(f) or key, f
|
||||
|
||||
data.add_field(key, fileobj, filename=filename)
|
||||
|
||||
@ -132,8 +159,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:
|
||||
@ -228,7 +254,7 @@ async def send_message(
|
||||
token, chat_id, text,
|
||||
disable_web_page_preview=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None,
|
||||
entities=None, allow_sending_without_reply=None):
|
||||
entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
"""
|
||||
Use this method to send text messages. On success, the sent Message is returned.
|
||||
:param token:
|
||||
@ -242,6 +268,7 @@ async def send_message(
|
||||
:param timeout:
|
||||
:param entities:
|
||||
:param allow_sending_without_reply:
|
||||
:param protect_content:
|
||||
:return:
|
||||
"""
|
||||
method_name = 'sendMessage'
|
||||
@ -262,10 +289,12 @@ async def send_message(
|
||||
params['entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
params['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
params['protect_content'] = protect_content
|
||||
|
||||
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'
|
||||
@ -325,6 +354,12 @@ async def delete_chat_sticker_set(token, chat_id):
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
async def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
|
||||
method_url = 'answerWebAppQuery'
|
||||
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
async def get_chat_member(token, chat_id, user_id):
|
||||
method_url = r'getChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
@ -333,19 +368,21 @@ async def get_chat_member(token, chat_id, user_id):
|
||||
|
||||
async def forward_message(
|
||||
token, chat_id, from_chat_id, message_id,
|
||||
disable_notification=None, timeout=None):
|
||||
disable_notification=None, timeout=None, protect_content=None):
|
||||
method_url = r'forwardMessage'
|
||||
payload = {'chat_id': chat_id, 'from_chat_id': from_chat_id, 'message_id': message_id}
|
||||
if disable_notification is not None:
|
||||
payload['disable_notification'] = disable_notification
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
async def copy_message(token, chat_id, from_chat_id, message_id, caption=None, parse_mode=None, caption_entities=None,
|
||||
disable_notification=None, reply_to_message_id=None, allow_sending_without_reply=None,
|
||||
reply_markup=None, timeout=None):
|
||||
reply_markup=None, timeout=None, protect_content=None):
|
||||
method_url = r'copyMessage'
|
||||
payload = {'chat_id': chat_id, 'from_chat_id': from_chat_id, 'message_id': message_id}
|
||||
if caption is not None:
|
||||
@ -364,13 +401,15 @@ async def copy_message(token, chat_id, from_chat_id, message_id, caption=None, p
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
async def send_dice(
|
||||
token, chat_id,
|
||||
emoji=None, disable_notification=None, reply_to_message_id=None,
|
||||
reply_markup=None, timeout=None, allow_sending_without_reply=None):
|
||||
reply_markup=None, timeout=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendDice'
|
||||
payload = {'chat_id': chat_id}
|
||||
if emoji:
|
||||
@ -385,6 +424,8 @@ async def send_dice(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -392,7 +433,7 @@ async def send_photo(
|
||||
token, chat_id, photo,
|
||||
caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None,
|
||||
caption_entities=None, allow_sending_without_reply=None):
|
||||
caption_entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendPhoto'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -418,13 +459,15 @@ async def send_photo(
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_media_group(
|
||||
token, chat_id, media,
|
||||
disable_notification=None, reply_to_message_id=None,
|
||||
timeout=None, allow_sending_without_reply=None):
|
||||
timeout=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendMediaGroup'
|
||||
media_json, files = await convert_input_media_array(media)
|
||||
payload = {'chat_id': chat_id, 'media': media_json}
|
||||
@ -436,6 +479,8 @@ async def send_media_group(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(
|
||||
token, method_url, params=payload,
|
||||
method='post' if files else 'get',
|
||||
@ -447,7 +492,7 @@ async def send_location(
|
||||
live_period=None, reply_to_message_id=None,
|
||||
reply_markup=None, disable_notification=None,
|
||||
timeout=None, horizontal_accuracy=None, heading=None,
|
||||
proximity_alert_radius=None, allow_sending_without_reply=None):
|
||||
proximity_alert_radius=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendLocation'
|
||||
payload = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude}
|
||||
if live_period:
|
||||
@ -468,6 +513,8 @@ async def send_location(
|
||||
payload['disable_notification'] = disable_notification
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -519,7 +566,7 @@ async def send_venue(
|
||||
foursquare_id=None, foursquare_type=None, disable_notification=None,
|
||||
reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None, google_place_id=None,
|
||||
google_place_type=None):
|
||||
google_place_type=None, protect_content=None):
|
||||
method_url = r'sendVenue'
|
||||
payload = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude, 'title': title, 'address': address}
|
||||
if foursquare_id:
|
||||
@ -540,13 +587,15 @@ async def send_venue(
|
||||
payload['google_place_id'] = google_place_id
|
||||
if google_place_type:
|
||||
payload['google_place_type'] = google_place_type
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
async def send_contact(
|
||||
token, chat_id, phone_number, first_name, last_name=None, vcard=None,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendContact'
|
||||
payload = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name}
|
||||
if last_name:
|
||||
@ -563,6 +612,8 @@ async def send_contact(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -576,7 +627,8 @@ async def send_chat_action(token, chat_id, action, timeout=None):
|
||||
|
||||
async def send_video(token, chat_id, data, duration=None, caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, supports_streaming=None, disable_notification=None, timeout=None,
|
||||
thumb=None, width=None, height=None, caption_entities=None, allow_sending_without_reply=None):
|
||||
thumb=None, width=None, height=None, caption_entities=None, allow_sending_without_reply=None,
|
||||
protect_content=None):
|
||||
method_url = r'sendVideo'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -616,13 +668,15 @@ async def send_video(token, chat_id, data, duration=None, caption=None, reply_to
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_animation(
|
||||
token, chat_id, data, duration=None, caption=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None, thumb=None, caption_entities=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, width=None, height=None, protect_content=None):
|
||||
method_url = r'sendAnimation'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -656,12 +710,18 @@ async def send_animation(
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if width:
|
||||
payload['width'] = width
|
||||
if height:
|
||||
payload['height'] = height
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_voice(token, chat_id, voice, caption=None, duration=None, reply_to_message_id=None, reply_markup=None,
|
||||
parse_mode=None, disable_notification=None, timeout=None, caption_entities=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendVoice'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -687,11 +747,13 @@ async def send_voice(token, chat_id, voice, caption=None, duration=None, reply_t
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_video_note(token, chat_id, data, duration=None, length=None, reply_to_message_id=None, reply_markup=None,
|
||||
disable_notification=None, timeout=None, thumb=None, allow_sending_without_reply=None):
|
||||
disable_notification=None, timeout=None, thumb=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendVideoNote'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -723,12 +785,14 @@ async def send_video_note(token, chat_id, data, duration=None, length=None, repl
|
||||
payload['thumb'] = thumb
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_audio(token, chat_id, audio, caption=None, duration=None, performer=None, title=None, reply_to_message_id=None,
|
||||
reply_markup=None, parse_mode=None, disable_notification=None, timeout=None, thumb=None,
|
||||
caption_entities=None, allow_sending_without_reply=None):
|
||||
caption_entities=None, allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendAudio'
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -766,12 +830,14 @@ async def send_audio(token, chat_id, audio, caption=None, duration=None, perform
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
|
||||
|
||||
async def send_data(token, chat_id, data, data_type, reply_to_message_id=None, reply_markup=None, parse_mode=None,
|
||||
disable_notification=None, timeout=None, caption=None, thumb=None, caption_entities=None,
|
||||
allow_sending_without_reply=None, disable_content_type_detection=None, visible_file_name=None):
|
||||
allow_sending_without_reply=None, disable_content_type_detection=None, visible_file_name=None, protect_content=None):
|
||||
method_url = await get_method_by_type(data_type)
|
||||
payload = {'chat_id': chat_id}
|
||||
files = None
|
||||
@ -806,6 +872,8 @@ async def send_data(token, chat_id, data, data_type, reply_to_message_id=None, r
|
||||
payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities))
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
if method_url == 'sendDocument' and disable_content_type_detection is not None:
|
||||
payload['disable_content_type_detection'] = disable_content_type_detection
|
||||
return await _process_request(token, method_url, params=payload, files=files, method='post')
|
||||
@ -876,7 +944,7 @@ async def promote_chat_member(
|
||||
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
|
||||
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
|
||||
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
|
||||
method_url = 'promoteChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
if can_change_info is not None:
|
||||
@ -899,8 +967,8 @@ async def promote_chat_member(
|
||||
payload['is_anonymous'] = is_anonymous
|
||||
if can_manage_chat is not None:
|
||||
payload['can_manage_chat'] = can_manage_chat
|
||||
if can_manage_voice_chats is not None:
|
||||
payload['can_manage_voice_chats'] = can_manage_voice_chats
|
||||
if can_manage_video_chats is not None:
|
||||
payload['can_manage_video_chats'] = can_manage_video_chats
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
@ -1041,6 +1109,42 @@ async def get_my_commands(token, scope=None, language_code=None):
|
||||
payload['language_code'] = language_code
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
async def set_chat_menu_button(token, chat_id=None, menu_button=None):
|
||||
method_url = r'setChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
if menu_button:
|
||||
payload['menu_button'] = menu_button.to_json()
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def get_chat_menu_button(token, chat_id=None):
|
||||
method_url = r'getChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
async def set_my_default_administrator_rights(token, rights=None, for_channels=None):
|
||||
method_url = r'setMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if rights:
|
||||
payload['rights'] = rights.to_json()
|
||||
if for_channels is not None:
|
||||
payload['for_channels'] = for_channels
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def get_my_default_administrator_rights(token, for_channels=None):
|
||||
method_url = r'getMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if for_channels:
|
||||
payload['for_channels'] = for_channels
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def set_my_commands(token, commands, scope=None, language_code=None):
|
||||
method_url = r'setMyCommands'
|
||||
@ -1136,7 +1240,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
|
||||
@ -1176,7 +1280,7 @@ async def delete_message(token, chat_id, message_id, timeout=None):
|
||||
async def send_game(
|
||||
token, chat_id, game_short_name,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None,
|
||||
allow_sending_without_reply=None):
|
||||
allow_sending_without_reply=None, protect_content=None):
|
||||
method_url = r'sendGame'
|
||||
payload = {'chat_id': chat_id, 'game_short_name': game_short_name}
|
||||
if disable_notification is not None:
|
||||
@ -1189,6 +1293,8 @@ async def send_game(
|
||||
payload['timeout'] = timeout
|
||||
if allow_sending_without_reply is not None:
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -1253,7 +1359,7 @@ async def send_invoice(
|
||||
need_name=None, need_phone_number=None, need_email=None, need_shipping_address=None,
|
||||
send_phone_number_to_provider = None, send_email_to_provider = None, is_flexible=None,
|
||||
disable_notification=None, reply_to_message_id=None, reply_markup=None, provider_data=None,
|
||||
timeout=None, allow_sending_without_reply=None, max_tip_amount=None, suggested_tip_amounts=None):
|
||||
timeout=None, allow_sending_without_reply=None, max_tip_amount=None, suggested_tip_amounts=None, protect_content=None):
|
||||
"""
|
||||
Use this method to send invoices. On success, the sent Message is returned.
|
||||
:param token: Bot's token (you don't need to fill this)
|
||||
@ -1285,6 +1391,7 @@ async def send_invoice(
|
||||
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
|
||||
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest units of the currency.
|
||||
At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount.
|
||||
:param protect_content:
|
||||
:return:
|
||||
"""
|
||||
method_url = r'sendInvoice'
|
||||
@ -1331,6 +1438,8 @@ async def send_invoice(
|
||||
payload['max_tip_amount'] = max_tip_amount
|
||||
if suggested_tip_amounts is not None:
|
||||
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@ -1428,11 +1537,16 @@ 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
|
||||
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}
|
||||
@ -1442,21 +1556,32 @@ 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
|
||||
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')
|
||||
|
||||
|
||||
@ -1479,7 +1604,7 @@ async def send_poll(
|
||||
is_anonymous = None, type = None, allows_multiple_answers = None, correct_option_id = None,
|
||||
explanation = None, explanation_parse_mode=None, open_period = None, close_date = None, is_closed = None,
|
||||
disable_notification=False, reply_to_message_id=None, allow_sending_without_reply=None,
|
||||
reply_markup=None, timeout=None, explanation_entities=None):
|
||||
reply_markup=None, timeout=None, explanation_entities=None, protect_content=None):
|
||||
method_url = r'sendPoll'
|
||||
payload = {
|
||||
'chat_id': str(chat_id),
|
||||
@ -1521,6 +1646,8 @@ async def send_poll(
|
||||
if explanation_entities:
|
||||
payload['explanation_entities'] = json.dumps(
|
||||
types.MessageEntity.to_list_of_dicts(explanation_entities))
|
||||
if protect_content:
|
||||
payload['protect_content'] = protect_content
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
async def _convert_list_json_serializable(results):
|
||||
@ -1643,4 +1770,10 @@ class ApiTelegramException(ApiException):
|
||||
function_name,
|
||||
result)
|
||||
self.result_json = result_json
|
||||
self.error_code = result_json['error_code']
|
||||
self.error_code = result_json['error_code']
|
||||
|
||||
class RequestTimeout(Exception):
|
||||
"""
|
||||
This class represents a request timeout.
|
||||
"""
|
||||
pass
|
13
telebot/asyncio_storage/__init__.py
Normal file
13
telebot/asyncio_storage/__init__.py
Normal 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'
|
||||
]
|
68
telebot/asyncio_storage/base_storage.py
Normal file
68
telebot/asyncio_storage/base_storage.py
Normal 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)
|
66
telebot/asyncio_storage/memory_storage.py
Normal file
66
telebot/asyncio_storage/memory_storage.py
Normal 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 hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
self.data[chat_id][user_id]['state'] = state
|
||||
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
|
108
telebot/asyncio_storage/pickle_storage.py
Normal file
108
telebot/asyncio_storage/pickle_storage.py
Normal file
@ -0,0 +1,108 @@
|
||||
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 hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
self.data[chat_id][user_id]['state'] = state
|
||||
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()
|
172
telebot/asyncio_storage/redis_storage.py
Normal file
172
telebot/asyncio_storage/redis_storage.py
Normal file
@ -0,0 +1,172 @@
|
||||
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 hasattr(state, 'name'):
|
||||
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
|
@ -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
|
||||
"""
|
||||
|
@ -1,12 +1,23 @@
|
||||
from abc import ABC
|
||||
from typing import Optional, Union
|
||||
from telebot.handler_backends import State
|
||||
|
||||
from telebot import types
|
||||
|
||||
|
||||
|
||||
|
||||
class SimpleCustomFilter(ABC):
|
||||
"""
|
||||
Simple Custom Filter base class.
|
||||
Create child class with check() method.
|
||||
Accepts only message, returns bool value, that is compared with given in handler.
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
@ -21,8 +32,12 @@ class AdvancedCustomFilter(ABC):
|
||||
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
|
||||
message: Message class
|
||||
text: Filter value given in handler
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
def check(self, message, text):
|
||||
"""
|
||||
Perform a check.
|
||||
@ -30,6 +45,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 +151,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 +172,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 +192,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 +206,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 +225,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 +241,6 @@ class IsReplyFilter(SimpleCustomFilter):
|
||||
return message.reply_to_message is not None
|
||||
|
||||
|
||||
|
||||
class LanguageFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check users language_code.
|
||||
@ -127,8 +253,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 +275,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 +283,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):
|
||||
"""
|
||||
|
201
telebot/formatting.py
Normal file
201
telebot/formatting.py
Normal file
@ -0,0 +1,201 @@
|
||||
"""
|
||||
Markdown & HTML formatting functions.
|
||||
|
||||
.. versionadded:: 4.5.1
|
||||
"""
|
||||
|
||||
import html
|
||||
import re
|
||||
|
||||
|
||||
def format_text(*args, separator="\n"):
|
||||
"""
|
||||
Formats a list of strings into a single string.
|
||||
|
||||
.. code:: python
|
||||
|
||||
format_text( # just an example
|
||||
mbold('Hello'),
|
||||
mitalic('World')
|
||||
)
|
||||
|
||||
:param args: Strings to format.
|
||||
:param separator: The separator to use between each string.
|
||||
"""
|
||||
return separator.join(args)
|
||||
|
||||
|
||||
|
||||
def escape_html(content: str) -> str:
|
||||
"""
|
||||
Escapes HTML characters in a string of HTML.
|
||||
|
||||
:param content: The string of HTML to escape.
|
||||
"""
|
||||
return html.escape(content)
|
||||
|
||||
|
||||
def escape_markdown(content: str) -> str:
|
||||
"""
|
||||
Escapes Markdown characters in a string of Markdown.
|
||||
|
||||
Credits: simonsmh
|
||||
|
||||
:param content: The string of Markdown to escape.
|
||||
"""
|
||||
|
||||
parse = re.sub(r"([_*\[\]()~`>\#\+\-=|\.!])", r"\\\1", content)
|
||||
reparse = re.sub(r"\\\\([_*\[\]()~`>\#\+\-=|\.!])", r"\1", parse)
|
||||
return reparse
|
||||
|
||||
|
||||
def mbold(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted bold string.
|
||||
|
||||
:param content: The string to bold.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '*{}*'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hbold(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted bold string.
|
||||
|
||||
:param content: The string to bold.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<b>{}</b>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mitalic(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted italic string.
|
||||
|
||||
:param content: The string to italicize.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '_{}_\r'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hitalic(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted italic string.
|
||||
|
||||
:param content: The string to italicize.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<i>{}</i>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def munderline(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted underline string.
|
||||
|
||||
:param content: The string to underline.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '__{}__'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hunderline(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted underline string.
|
||||
|
||||
:param content: The string to underline.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<u>{}</u>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mstrikethrough(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted strikethrough string.
|
||||
|
||||
:param content: The string to strikethrough.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '~{}~'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hstrikethrough(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted strikethrough string.
|
||||
|
||||
:param content: The string to strikethrough.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<s>{}</s>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mspoiler(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted spoiler string.
|
||||
|
||||
:param content: The string to spoiler.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '||{}||'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hspoiler(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted spoiler string.
|
||||
|
||||
:param content: The string to spoiler.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<tg-spoiler>{}</tg-spoiler>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mlink(content: str, url: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted link string.
|
||||
|
||||
:param content: The string to link.
|
||||
:param url: The URL to link to.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '[{}]({})'.format(escape_markdown(content), escape_markdown(url) if escape else content)
|
||||
|
||||
|
||||
def hlink(content: str, url: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted link string.
|
||||
|
||||
:param content: The string to link.
|
||||
:param url: The URL to link to.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<a href="{}">{}</a>'.format(escape_html(url), escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mcode(content: str, language: str="", escape: bool=False) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted code string.
|
||||
|
||||
:param content: The string to code.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '```{}\n{}```'.format(language, escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hcode(content: str, escape: bool=False) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted code string.
|
||||
|
||||
:param content: The string to code.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<code>{}</code>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def hpre(content: str, escape: bool=False, language: str="") -> str:
|
||||
"""
|
||||
Returns an HTML-formatted preformatted string.
|
||||
|
||||
:param content: The string to preformatted.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<pre><code class="{}">{}</code></pre>'.format(language, escape_html(content) if escape else content)
|
@ -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,200 +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:
|
||||
new_data = 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
|
13
telebot/storage/__init__.py
Normal file
13
telebot/storage/__init__.py
Normal 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'
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user