mirror of
https://github.com/eternnoir/pyTelegramBotAPI.git
synced 2023-08-10 21:12:57 +03:00
Compare commits
130 Commits
4.4.1
...
b9b4885568
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b4885568 | ||
|
|
ce0a974c91 | ||
|
|
6ff015689a | ||
|
|
6459aded82 | ||
|
|
fec47cecaf | ||
|
|
3892b0fb80 | ||
|
|
fbb9a73fc0 | ||
|
|
db5c53b8e5 | ||
|
|
6e8abc709e | ||
|
|
752f35614c | ||
|
|
a2893945b2 | ||
|
|
1686ce4f44 | ||
|
|
24b1129c8a | ||
|
|
7f9dac4147 | ||
|
|
f96775e9eb | ||
|
|
1342cab259 | ||
|
|
dd8125cbd0 | ||
|
|
8769802744 | ||
|
|
a7fb8a2bec | ||
|
|
d7f34ae370 | ||
|
|
7d931abe37 | ||
|
|
f52124827f | ||
|
|
0d0f9ef330 | ||
|
|
24cdcb1bcc | ||
|
|
7994a80a00 | ||
|
|
b2662274f9 | ||
|
|
a21ab203a1 | ||
|
|
e689e968db | ||
|
|
808810e6d6 | ||
|
|
5a60846c7f | ||
|
|
d2c1615392 | ||
|
|
7cdb48c3e0 | ||
|
|
28ae0867f8 | ||
|
|
e84cc0e0af | ||
|
|
42ce47914d | ||
|
|
4401780ba9 | ||
|
|
82f056e88a | ||
|
|
6b19d631d1 | ||
|
|
ee7adb00df | ||
|
|
82a8aa65f0 | ||
|
|
72aaf44dc7 | ||
|
|
1d0efce76e | ||
|
|
74d0604c05 | ||
|
|
1943f659bc | ||
|
|
d6ec104829 | ||
|
|
7edaa51995 | ||
|
|
6bb47e9a44 | ||
|
|
8da749ee05 | ||
|
|
0c59d1187e | ||
|
|
e9d1d98f03 | ||
|
|
c63b0d6a3d | ||
|
|
388306b7fe | ||
|
|
5e3fd17436 | ||
|
|
ccc09ffaf3 | ||
|
|
e086303535 | ||
|
|
d6e93f85f1 | ||
|
|
d954f8f5b3 | ||
|
|
33375ac135 | ||
|
|
28662876a2 | ||
|
|
0f44fd3c40 | ||
|
|
8fefd7b5b3 | ||
|
|
7567750276 | ||
|
|
f526a9d8a4 | ||
|
|
feffe726dd | ||
|
|
3ff7e28467 | ||
|
|
5d74e18d1a | ||
|
|
91b665ea94 | ||
|
|
6e4c63b09c | ||
|
|
42efb8488c | ||
|
|
03a567ea93 | ||
|
|
3f28bb6e9d | ||
|
|
e051dda113 | ||
|
|
856debe7d2 | ||
|
|
42955d1886 | ||
|
|
a2f3cd03e1 | ||
|
|
7e68721475 | ||
|
|
7965ba4f69 | ||
|
|
aba4d3e246 | ||
|
|
caae6bb93f | ||
|
|
096d7a4eea | ||
|
|
ab4140ba9f | ||
|
|
77c3587012 | ||
|
|
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 | ||
|
|
43d0e10ba4 | ||
|
|
9652fdbecb |
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -10,6 +10,5 @@ OS:
|
||||
|
||||
## Checklist:
|
||||
- [ ] I added/edited example on new feature/change (if exists)
|
||||
- [ ] My changes won't break backend compatibility
|
||||
- [ ] I made changes for async and sync
|
||||
|
||||
- [ ] My changes won't break backward compatibility
|
||||
- [ ] I made changes both for sync and async
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -64,6 +64,7 @@ testMain.py
|
||||
#VS Code
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
|
||||
# documentation
|
||||
_build/
|
||||
|
||||
42
README.md
42
README.md
@@ -11,7 +11,7 @@
|
||||
<p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p>
|
||||
<p align="center">Both synchronous and asynchronous.</p>
|
||||
|
||||
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
|
||||
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#june-20-2022">6.1</a>!
|
||||
|
||||
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
* [Telegram Channel](#telegram-channel)
|
||||
* [More examples](#more-examples)
|
||||
* [Code Template](#code-template)
|
||||
* [Bots using this API](#bots-using-this-api)
|
||||
* [Bots using this library](#bots-using-this-library)
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -358,7 +358,25 @@ def start(message):
|
||||
There are other examples using middleware handler in the [examples/middleware](examples/middleware) directory.
|
||||
|
||||
#### Class-based middlewares
|
||||
There are class-based middlewares. Check out in [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/middleware/class_based)
|
||||
There are class-based middlewares.
|
||||
Basic class-based middleware looks like this:
|
||||
```python
|
||||
class Middleware(BaseMiddleware):
|
||||
def __init__(self):
|
||||
self.update_types = ['message']
|
||||
def pre_process(self, message, data):
|
||||
data['foo'] = 'Hello' # just for example
|
||||
# we edited the data. now, this data is passed to handler.
|
||||
# return SkipHandler() -> this will skip handler
|
||||
# return CancelUpdate() -> this will cancel update
|
||||
def post_process(self, message, data, exception=None):
|
||||
print(data['foo'])
|
||||
if exception: # check for exception
|
||||
print(exception)
|
||||
```
|
||||
Class-based middleware should have to functions: post and pre process.
|
||||
So, as you can see, class-based middlewares work before and after handler execution.
|
||||
For more, check out in [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/middleware/class_based)
|
||||
|
||||
#### Custom filters
|
||||
Also, you can use built-in custom filters. Or, you can create your own filter.
|
||||
@@ -656,6 +674,8 @@ telebot.logger.setLevel(logging.DEBUG) # Outputs debug messages to console.
|
||||
```
|
||||
|
||||
### Proxy
|
||||
For sync:
|
||||
|
||||
You can use proxy for request. `apihelper.proxy` object will use by call `requests` proxies argument.
|
||||
|
||||
```python
|
||||
@@ -670,6 +690,14 @@ If you want to use socket5 proxy you need install dependency `pip install reques
|
||||
apihelper.proxy = {'https':'socks5://userproxy:password@proxy_address:port'}
|
||||
```
|
||||
|
||||
For async:
|
||||
```python
|
||||
from telebot import asyncio_helper
|
||||
|
||||
asyncio_helper.proxy = 'http://127.0.0.1:3128' #url
|
||||
```
|
||||
|
||||
|
||||
### Testing
|
||||
You can disable or change the interaction with real Telegram server by using
|
||||
```python
|
||||
@@ -699,7 +727,8 @@ Result will be:
|
||||
|
||||
|
||||
## API conformance
|
||||
|
||||
* ✔ [Bot API 6.1](https://core.telegram.org/bots/api#june-20-2022)
|
||||
* ✔ [Bot API 6.0](https://core.telegram.org/bots/api#april-16-2022)
|
||||
* ✔ [Bot API 5.7](https://core.telegram.org/bots/api#january-31-2022)
|
||||
* ✔ [Bot API 5.6](https://core.telegram.org/bots/api#december-30-2021)
|
||||
* ✔ [Bot API 5.5](https://core.telegram.org/bots/api#december-7-2021)
|
||||
@@ -816,7 +845,7 @@ Here are some examples of template:
|
||||
* [TeleBot template](https://github.com/coder2020official/telebot_template)
|
||||
|
||||
|
||||
## Bots using this API
|
||||
## Bots using this library
|
||||
* [SiteAlert bot](https://telegram.me/SiteAlert_bot) ([source](https://github.com/ilteoood/SiteAlert-Python)) by *ilteoood* - Monitors websites and sends a notification on changes
|
||||
* [TelegramLoggingBot](https://github.com/aRandomStranger/TelegramLoggingBot) by *aRandomStranger*
|
||||
* [Telegram LMGTFY_bot](https://github.com/GabrielRF/telegram-lmgtfy_bot) by *GabrielRF* - Let me Google that for you.
|
||||
@@ -867,5 +896,8 @@ Here are some examples of template:
|
||||
* [remoteTelegramShell](https://github.com/EnriqueMoran/remoteTelegramShell) by [EnriqueMoran](https://github.com/EnriqueMoran). Control your LinuxOS computer through Telegram.
|
||||
* [Pyfram-telegram-bot](https://github.com/skelly37/pyfram-telegram-bot) Query wolframalpha.com and make use of its API through Telegram.
|
||||
* [TranslateThisVideoBot](https://gitlab.com/WuerfelDev/translatethisvideo) This Bot can understand spoken text in videos and translate it to English
|
||||
* [Zyprexa](https://t.me/mathemathicsBot) ([source](https://github.com/atif5/zyprexa)) Zyprexa can solve, help you solve any mathematical problem you encounter and convert your regular mathematical expressions into beautiful imagery using LaTeX.
|
||||
* [Bincode-telegram-bot](https://github.com/tusharhero/bincode-telegram-bot) by [tusharhero](https://github.com/tusharhero) - Makes [bincodes](https://github.com/tusharhero/bincode) from text provides and also converts them back to text.
|
||||
* [hydrolib_bot](https://github.com/Mayson90/hydrolib_bot) Toolset for Hydrophilia tabletop game (game cards, rules, structure...).
|
||||
|
||||
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
AsyncTeleBot
|
||||
====================
|
||||
|
||||
|
||||
.. meta::
|
||||
:description: Asynchronous pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, asynctelebot, documentation
|
||||
|
||||
|
||||
AsyncTeleBot methods
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
Callback data factory
|
||||
=====================
|
||||
|
||||
.. meta::
|
||||
:description: Callback data factory in pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, callbackdatafactory, guide, callbackdata, factory
|
||||
|
||||
|
||||
callback\_data file
|
||||
-----------------------------
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'pyTelegramBotAPI'
|
||||
project = 'pyTelegramBotAPI Documentation'
|
||||
copyright = '2022, coder2020official'
|
||||
author = 'coder2020official'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '4.4.1'
|
||||
release = '4.6.0'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
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:
|
||||
@@ -7,6 +7,11 @@
|
||||
Welcome to pyTelegramBotAPI's documentation!
|
||||
============================================
|
||||
|
||||
.. meta::
|
||||
:description: Official documentation of pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, documentation, guide
|
||||
|
||||
|
||||
=======
|
||||
TeleBot
|
||||
=======
|
||||
@@ -49,6 +54,7 @@ Content
|
||||
async_version/index
|
||||
calldata
|
||||
util
|
||||
formatting
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
Installation Guide
|
||||
==================
|
||||
|
||||
.. meta::
|
||||
:description: Installation of pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, installation, guide
|
||||
|
||||
|
||||
Using PIP
|
||||
----------
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
Quick start
|
||||
===========
|
||||
|
||||
.. meta::
|
||||
:description: Quickstart guide
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, quickstart, guide
|
||||
|
||||
Synchronous TeleBot
|
||||
-------------------
|
||||
.. literalinclude:: ../../examples/echo_bot.py
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
TeleBot version
|
||||
===============
|
||||
|
||||
.. meta::
|
||||
:description: Synchronous pyTelegramBotAPI documentation
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, methods, guide, files, sync
|
||||
|
||||
TeleBot methods
|
||||
---------------
|
||||
.. automodule:: telebot
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
Utils
|
||||
============
|
||||
|
||||
.. meta::
|
||||
:description: Utils in pyTelegramBotAPI
|
||||
:keywords: ptba, pytba, pyTelegramBotAPI, utils, guide
|
||||
|
||||
|
||||
util file
|
||||
-------------------
|
||||
|
||||
@@ -23,8 +23,8 @@ async def my_chat_m(message: types.ChatMemberUpdated):
|
||||
#content_Type_service is:
|
||||
#'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
|
||||
#'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
||||
#'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
|
||||
#'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
#'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
|
||||
#'video_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
# this handler deletes service messages
|
||||
|
||||
@bot.message_handler(content_types=util.content_type_service)
|
||||
|
||||
54
examples/asynchronous_telebot/formatting_example.py
Normal file
54
examples/asynchronous_telebot/formatting_example.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot import formatting
|
||||
|
||||
bot = AsyncTeleBot('token')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
async def start_message(message):
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
# function which connects all strings
|
||||
formatting.format_text(
|
||||
formatting.mbold(message.from_user.first_name),
|
||||
formatting.mitalic(message.from_user.first_name),
|
||||
formatting.munderline(message.from_user.first_name),
|
||||
formatting.mstrikethrough(message.from_user.first_name),
|
||||
formatting.mcode(message.from_user.first_name),
|
||||
separator=" " # separator separates all strings
|
||||
),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# just a bold text using markdownv2
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.mbold(message.from_user.first_name),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# html
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.format_text(
|
||||
formatting.hbold(message.from_user.first_name),
|
||||
formatting.hitalic(message.from_user.first_name),
|
||||
formatting.hunderline(message.from_user.first_name),
|
||||
formatting.hstrikethrough(message.from_user.first_name),
|
||||
formatting.hcode(message.from_user.first_name),
|
||||
# hide_link is only for html
|
||||
formatting.hide_link('https://telegra.ph/file/c158e3a6e2a26a160b253.jpg'),
|
||||
separator=" "
|
||||
),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# just a bold text in html
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.hbold(message.from_user.first_name),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
import asyncio
|
||||
asyncio.run(bot.polling())
|
||||
@@ -56,7 +56,7 @@ import keyboards
|
||||
from telebot import types
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.asyncio_filters import TextMatchFilter, TextFilter
|
||||
from i18n_base_midddleware import I18N
|
||||
from i18n_base_middleware import I18N
|
||||
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
|
||||
|
||||
|
||||
|
||||
17
examples/asynchronous_telebot/multibot/README.MD
Normal file
17
examples/asynchronous_telebot/multibot/README.MD
Normal file
@@ -0,0 +1,17 @@
|
||||
You probably have seen bots which allow you to send them token of your bot and then handle updates providing some functionality for your bot.
|
||||
<br>
|
||||
This type of bots are called <b>multibots</b>. They are created using webhooks.
|
||||
<br>
|
||||
|
||||
This is the example of simple multibot.<br>
|
||||
In order to reproduce this example you need to have <b>domain and ssl connection</b>.
|
||||
<br>
|
||||
If you have, go to config.py and specify your data.
|
||||
<br>
|
||||
There is also file called <b>nginx_conf.conf</b>, we will use nginx as proxy-server and this file is example nginx.conf file.
|
||||
<br>
|
||||
Make sure that server_name and port are the same in both config and nginx_conf
|
||||
<br>
|
||||
(nginx_conf.conf IS NOT complete, you would probably use tools like certbot to add ssl connection to it)
|
||||
<br>
|
||||
Also, in this example I used dictionary as tokens storage, but in production you should use database so that you can re-set webhooks in case bot restarts.
|
||||
6
examples/asynchronous_telebot/multibot/config.py
Normal file
6
examples/asynchronous_telebot/multibot/config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
MAIN_BOT_TOKEN = "your_main_bot_token"
|
||||
|
||||
WEBHOOK_HOST = "your_domain.com"
|
||||
WEBHOOK_PATH = "telegram_webhook"
|
||||
WEBAPP_HOST = "0.0.0.0"
|
||||
WEBAPP_PORT = 3500
|
||||
15
examples/asynchronous_telebot/multibot/handlers.py
Normal file
15
examples/asynchronous_telebot/multibot/handlers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot import types
|
||||
|
||||
|
||||
async def hello_handler(message: types.Message, bot: AsyncTeleBot):
|
||||
await bot.send_message(message.chat.id, "Hi :)")
|
||||
|
||||
|
||||
async def echo_handler(message: types.Message, bot: AsyncTeleBot):
|
||||
await bot.send_message(message.chat.id, message.text)
|
||||
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
bot.register_message_handler(hello_handler, func=lambda message: message.text == 'Hello', pass_bot=True)
|
||||
bot.register_message_handler(echo_handler, pass_bot=True)
|
||||
56
examples/asynchronous_telebot/multibot/main.py
Normal file
56
examples/asynchronous_telebot/multibot/main.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
from telebot import types, util
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from handlers import register_handlers
|
||||
|
||||
import config
|
||||
|
||||
main_bot = AsyncTeleBot(config.MAIN_BOT_TOKEN)
|
||||
app = web.Application()
|
||||
tokens = {config.MAIN_BOT_TOKEN: True}
|
||||
|
||||
|
||||
async def webhook(request):
|
||||
token = request.match_info.get('token')
|
||||
if not tokens.get(token):
|
||||
return web.Response(status=404)
|
||||
|
||||
if request.headers.get('content-type') != 'application/json':
|
||||
return web.Response(status=403)
|
||||
|
||||
json_string = await request.json()
|
||||
update = types.Update.de_json(json_string)
|
||||
if token == main_bot.token:
|
||||
await main_bot.process_new_updates([update])
|
||||
return web.Response()
|
||||
|
||||
from_update_bot = AsyncTeleBot(token)
|
||||
register_handlers(from_update_bot)
|
||||
await from_update_bot.process_new_updates([update])
|
||||
return web.Response()
|
||||
|
||||
|
||||
app.router.add_post("/" + config.WEBHOOK_PATH + "/{token}", webhook)
|
||||
|
||||
|
||||
@main_bot.message_handler(commands=['add_bot'])
|
||||
async def add_bot(message: types.Message):
|
||||
token = util.extract_arguments(message.text)
|
||||
tokens[token] = True
|
||||
|
||||
new_bot = AsyncTeleBot(token)
|
||||
await new_bot.delete_webhook()
|
||||
await new_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{token}")
|
||||
|
||||
await new_bot.send_message(message.chat.id, "Webhook was set.")
|
||||
|
||||
|
||||
async def main():
|
||||
await main_bot.delete_webhook()
|
||||
await main_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{config.MAIN_BOT_TOKEN}")
|
||||
web.run_app(app, host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
8
examples/asynchronous_telebot/multibot/nginx_conf.conf
Normal file
8
examples/asynchronous_telebot/multibot/nginx_conf.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
server_name your_domain.com;
|
||||
|
||||
location /telegram_webhook/ {
|
||||
proxy_pass http://localhost:3500;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
33
examples/create_invite_link.py
Normal file
33
examples/create_invite_link.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import telebot
|
||||
from time import sleep, time
|
||||
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton #Only for creating Inline Buttons, not necessary for creating Invite Links
|
||||
|
||||
Token = "api_token" #Your Bot Access Token
|
||||
Group_ID = -1234567890 #Group ID for which invite link is to be created
|
||||
|
||||
bot = telebot.TeleBot(Token, parse_mode="HTML")
|
||||
|
||||
#/start command message
|
||||
@bot.message_handler(commands=['start'])
|
||||
def startmsg(msg):
|
||||
bot.reply_to(msg, "Hey there, I'm a bot made by pyTelegramBotAPI!")
|
||||
|
||||
#Get notified of incoming members in group
|
||||
@bot.message_handler(content_types=['new_chat_members'])
|
||||
def newmember(msg):
|
||||
#Create an invite link class that contains info about the created invite link using create_chat_invite_link() with parameters
|
||||
invite = bot.create_chat_invite_link(Group_ID, member_limit=1, expire_date=int(time())+45) #Here, the link will auto-expire in 45 seconds
|
||||
InviteLink = invite.invite_link #Get the actual invite link from 'invite' class
|
||||
|
||||
mrkplink = InlineKeyboardMarkup() #Created Inline Keyboard Markup
|
||||
mrkplink.add(InlineKeyboardButton("Join our group 🚀", url=InviteLink)) #Added Invite Link to Inline Keyboard
|
||||
|
||||
bot.send_message(msg.chat.id, f"Hey there {msg.from_user.first_name}, Click the link below to join our Official Group.", reply_markup=mrkplink)
|
||||
|
||||
#This will send a message with the newly-created invite link as markup button.
|
||||
#The member limit will be 1 and expiring time will be 45 sec.
|
||||
|
||||
|
||||
|
||||
|
||||
bot.infinity_polling()
|
||||
@@ -60,7 +60,7 @@ def name_get(message):
|
||||
"""
|
||||
State 1. Will process when user's state is MyStates.name.
|
||||
"""
|
||||
bot.send_message(message.chat.id, f'Now write me a surname')
|
||||
bot.send_message(message.chat.id, 'Now write me a surname')
|
||||
bot.set_state(message.from_user.id, MyStates.surname, message.chat.id)
|
||||
with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
|
||||
data['name'] = message.text
|
||||
@@ -83,7 +83,11 @@ def ready_for_answer(message):
|
||||
State 3. Will process when user's state is MyStates.age.
|
||||
"""
|
||||
with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
|
||||
bot.send_message(message.chat.id, "Ready, take a look:\n<b>Name: {name}\nSurname: {surname}\nAge: {age}</b>".format(name=data['name'], surname=data['surname'], age=message.text), parse_mode="html")
|
||||
msg = ("Ready, take a look:\n<b>"
|
||||
f"Name: {data['name']}\n"
|
||||
f"Surname: {data['surname']}\n"
|
||||
f"Age: {message.text}</b>")
|
||||
bot.send_message(message.chat.id, msg, parse_mode="html")
|
||||
bot.delete_state(message.from_user.id, message.chat.id)
|
||||
|
||||
#incorrect number
|
||||
@@ -99,4 +103,4 @@ def age_incorrect(message):
|
||||
bot.add_custom_filter(custom_filters.StateFilter(bot))
|
||||
bot.add_custom_filter(custom_filters.IsDigitFilter())
|
||||
|
||||
bot.infinity_polling(skip_pending=True)
|
||||
bot.infinity_polling(skip_pending=True)
|
||||
|
||||
53
examples/formatting_example.py
Normal file
53
examples/formatting_example.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from telebot import TeleBot
|
||||
from telebot import formatting
|
||||
|
||||
bot = TeleBot('TOKEN')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_message(message):
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
# function which connects all strings
|
||||
formatting.format_text(
|
||||
formatting.mbold(message.from_user.first_name),
|
||||
formatting.mitalic(message.from_user.first_name),
|
||||
formatting.munderline(message.from_user.first_name),
|
||||
formatting.mstrikethrough(message.from_user.first_name),
|
||||
formatting.mcode(message.from_user.first_name),
|
||||
separator=" " # separator separates all strings
|
||||
),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# just a bold text using markdownv2
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.mbold(message.from_user.first_name),
|
||||
parse_mode='MarkdownV2'
|
||||
)
|
||||
|
||||
# html
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.format_text(
|
||||
formatting.hbold(message.from_user.first_name),
|
||||
formatting.hitalic(message.from_user.first_name),
|
||||
formatting.hunderline(message.from_user.first_name),
|
||||
formatting.hstrikethrough(message.from_user.first_name),
|
||||
formatting.hcode(message.from_user.first_name),
|
||||
# hide_link is only for html
|
||||
formatting.hide_link('https://telegra.ph/file/c158e3a6e2a26a160b253.jpg'),
|
||||
separator=" "
|
||||
),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# just a bold text in html
|
||||
bot.send_message(
|
||||
message.chat.id,
|
||||
formatting.hbold(message.from_user.first_name),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
bot.infinity_polling()
|
||||
@@ -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_middleware import I18N
|
||||
from telebot import TeleBot
|
||||
from telebot import types, StateMemoryStorage
|
||||
from telebot.custom_filters import TextMatchFilter, TextFilter
|
||||
|
||||
class I18NMiddleware(I18N):
|
||||
|
||||
def process_update_types(self) -> list:
|
||||
"""
|
||||
Here you need to return a list of update types which you want to be processed
|
||||
"""
|
||||
return ['message', 'callback_query']
|
||||
|
||||
def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]):
|
||||
"""
|
||||
This method is called when new update comes (only updates which you return in 'process_update_types' method)
|
||||
Returned language will be used in 'pre_process' method of parent class
|
||||
Returned language will be set to context language variable.
|
||||
If you need to get translation with user's actual language you don't have to pass it manually
|
||||
It will be automatically passed from context language value.
|
||||
However if you need some other language you can always pass it.
|
||||
"""
|
||||
|
||||
user_id = obj.from_user.id
|
||||
|
||||
if user_id not in users_lang:
|
||||
users_lang[user_id] = 'en'
|
||||
|
||||
return users_lang[user_id]
|
||||
|
||||
|
||||
storage = StateMemoryStorage()
|
||||
bot = TeleBot("", state_storage=storage, use_class_middlewares=True)
|
||||
|
||||
i18n = I18NMiddleware(translations_path='locales', domain_name='messages')
|
||||
_ = i18n.gettext # for singular translations
|
||||
__ = i18n.ngettext # for plural translations
|
||||
|
||||
# These are example storages, do not use it in a production development
|
||||
users_lang = {}
|
||||
users_clicks = {}
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_handler(message: types.Message):
|
||||
text = _("Hello, {user_fist_name}!\n"
|
||||
"This is the example of multilanguage bot.\n"
|
||||
"Available commands:\n\n"
|
||||
"/lang - change your language\n"
|
||||
"/plural - pluralization example\n"
|
||||
"/menu - text menu example")
|
||||
|
||||
# remember don't use f string for interpolation, use .format method instead
|
||||
text = text.format(user_fist_name=message.from_user.first_name)
|
||||
bot.send_message(message.from_user.id, text)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['lang'])
|
||||
def change_language_handler(message: types.Message):
|
||||
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
|
||||
reply_markup=keyboards.languages_keyboard())
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn']))
|
||||
def language_handler(call: types.CallbackQuery):
|
||||
lang = call.data
|
||||
users_lang[call.from_user.id] = lang
|
||||
|
||||
# When you changed user language, you have to pass it manually beacause it is not changed in context
|
||||
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['plural'])
|
||||
def pluralization_handler(message: types.Message):
|
||||
if not users_clicks.get(message.from_user.id):
|
||||
users_clicks[message.from_user.id] = 0
|
||||
clicks = users_clicks[message.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
|
||||
def click_handler(call: types.CallbackQuery):
|
||||
if not users_clicks.get(call.from_user.id):
|
||||
users_clicks[call.from_user.id] = 1
|
||||
else:
|
||||
users_clicks[call.from_user.id] += 1
|
||||
|
||||
clicks = users_clicks[call.from_user.id]
|
||||
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
|
||||
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||
reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.message_handler(commands=['menu'])
|
||||
def menu_handler(message: types.Message):
|
||||
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_))
|
||||
|
||||
|
||||
# For lazy tranlations
|
||||
# lazy gettext is used when you don't know user's locale
|
||||
# It can be used for example to handle text buttons in multilanguage bot
|
||||
# The actual translation will be delayed until update comes and context language is set
|
||||
l_ = i18n.lazy_gettext
|
||||
|
||||
|
||||
# Handlers below will handle text according to user's language
|
||||
@bot.message_handler(text=l_("My user id"))
|
||||
def return_user_id(message: types.Message):
|
||||
bot.send_message(message.chat.id, str(message.from_user.id))
|
||||
|
||||
|
||||
@bot.message_handler(text=l_("My user name"))
|
||||
def return_user_id(message: types.Message):
|
||||
username = message.from_user.username
|
||||
if not username:
|
||||
username = '-'
|
||||
bot.send_message(message.chat.id, username)
|
||||
|
||||
|
||||
# You can make it case-insensitive
|
||||
@bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True))
|
||||
def return_user_id(message: types.Message):
|
||||
bot.send_message(message.chat.id, message.from_user.first_name)
|
||||
|
||||
|
||||
all_menu_texts = []
|
||||
for language in i18n.available_translations:
|
||||
for menu_text in ("My user id", "My user name", "My first name"):
|
||||
all_menu_texts.append(_(menu_text, language))
|
||||
|
||||
|
||||
# When user confused language. (handles all menu buttons texts)
|
||||
@bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True))
|
||||
def missed_message(message: types.Message):
|
||||
bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.setup_middleware(i18n)
|
||||
bot.add_custom_filter(TextMatchFilter())
|
||||
asyncio.run(bot.infinity_polling())
|
||||
@@ -1,5 +1,6 @@
|
||||
import gettext
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
class I18N:
|
||||
@@ -8,6 +9,8 @@ class I18N:
|
||||
It is based on gettext util.
|
||||
"""
|
||||
|
||||
context_lang = threading.local()
|
||||
|
||||
def __init__(self, translations_path, domain_name: str):
|
||||
self.path = translations_path
|
||||
self.domain = domain_name
|
||||
@@ -21,7 +24,11 @@ class I18N:
|
||||
"""
|
||||
Singular translations
|
||||
"""
|
||||
if not lang or lang not in self.translations:
|
||||
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
return text
|
||||
|
||||
translator = self.translations[lang]
|
||||
@@ -31,7 +38,10 @@ class I18N:
|
||||
"""
|
||||
Plural translations
|
||||
"""
|
||||
if not lang or lang not in self.translations:
|
||||
if lang is None:
|
||||
lang = self.context_lang.language
|
||||
|
||||
if lang not in self.translations:
|
||||
if n == 1:
|
||||
return singular
|
||||
return plural
|
||||
@@ -39,6 +49,7 @@ class I18N:
|
||||
translator = self.translations[lang]
|
||||
return translator.ngettext(singular, plural, n)
|
||||
|
||||
|
||||
def find_translations(self):
|
||||
"""
|
||||
Looks for translations with passed 'domain' in passed 'path'
|
||||
@@ -13,11 +13,11 @@ def languages_keyboard():
|
||||
)
|
||||
|
||||
|
||||
def clicker_keyboard(_, lang):
|
||||
def clicker_keyboard(_):
|
||||
return InlineKeyboardMarkup(
|
||||
keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
|
||||
InlineKeyboardButton(text=_("click"), callback_data='click'),
|
||||
]
|
||||
]
|
||||
)
|
||||
@@ -47,5 +47,4 @@ msgstr[1] ""
|
||||
msgid ""
|
||||
"This is clicker.\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
msgstr ""
|
||||
@@ -56,5 +56,4 @@ msgid ""
|
||||
"\n"
|
||||
msgstr ""
|
||||
"Это кликер.\n"
|
||||
"\n"
|
||||
|
||||
"\n"
|
||||
@@ -55,4 +55,3 @@ msgid ""
|
||||
msgstr ""
|
||||
"Bu clicker.\n"
|
||||
"\n"
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
"""
|
||||
In this example you will learn how to adapt your bot to different languages
|
||||
Using built-in class I18N.
|
||||
|
||||
You need to install babel package 'https://pypi.org/project/Babel/'
|
||||
Babel provides a command-line interface for working with message catalogs
|
||||
After installing babel package you have a script called 'pybabel'
|
||||
Too see all the commands open terminal and type 'pybabel --help'
|
||||
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
|
||||
|
||||
Create a directory 'locales' where our translations will be stored
|
||||
|
||||
First we need to extract texts:
|
||||
pybabel extract -o locales/{domain_name}.pot --input-dirs .
|
||||
{domain_name}.pot - is the file where all translations are saved
|
||||
The name of this file should be the same as domain which you pass to I18N class
|
||||
In this example domain_name will be 'messages'
|
||||
|
||||
For gettext (singular texts) we use '_' alias and it works perfect
|
||||
You may also you some alias for ngettext (plural texts) but you can face with a problem that
|
||||
your plural texts are not being extracted
|
||||
@@ -23,25 +19,20 @@ That is because by default 'pybabel extract' recognizes the following keywords:
|
||||
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
|
||||
To add your own keyword you can use '-k' flag
|
||||
In this example for 'ngettext' i will assign double underscore alias '__'
|
||||
|
||||
Full command with pluralization support will look so:
|
||||
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||
|
||||
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l en
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l ru
|
||||
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
|
||||
|
||||
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
|
||||
After you translated all the texts you need to compile .po files:
|
||||
pybabel compile -d locales
|
||||
|
||||
When you delete/update your texts you also need to update them in .po files:
|
||||
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||
pybabel update -i locales/{domain_name}.pot -d locales
|
||||
- translate
|
||||
pybabel compile -d locales
|
||||
|
||||
If you have any exceptions check:
|
||||
- you have installed babel
|
||||
- translations are ready, so you just compiled it
|
||||
@@ -49,14 +40,17 @@ If you have any exceptions check:
|
||||
- you are writing commands from correct path in terminal
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import keyboards
|
||||
from telebot import TeleBot, types, custom_filters
|
||||
from telebot import apihelper
|
||||
from telebot.storage.memory_storage import StateMemoryStorage
|
||||
|
||||
import keyboards
|
||||
from i18n_class import I18N
|
||||
|
||||
apihelper.ENABLE_MIDDLEWARE = True
|
||||
storage = StateMemoryStorage()
|
||||
bot = TeleBot("", state_storage=storage)
|
||||
# IMPORTANT! This example works only if polling is non-threaded.
|
||||
bot = TeleBot("", state_storage=storage, threaded=False)
|
||||
|
||||
i18n = I18N(translations_path='locales', domain_name='messages')
|
||||
_ = i18n.gettext # for singular translations
|
||||
@@ -67,35 +61,25 @@ users_lang = {}
|
||||
users_clicks = {}
|
||||
|
||||
|
||||
def get_user_language(func):
|
||||
"""
|
||||
This decorator will pass to your handler current user's language
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
obj = args[0]
|
||||
kwargs.update(lang=users_lang.get(obj.from_user.id, 'en'))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
@bot.middleware_handler(update_types=['message', 'callback_query'])
|
||||
def set_contex_language(bot_instance, message):
|
||||
i18n.context_lang.language = users_lang.get(message.from_user.id, 'en')
|
||||
|
||||
|
||||
@bot.message_handler(commands='start')
|
||||
@get_user_language
|
||||
def start_handler(message: types.Message, lang):
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start_handler(message: types.Message):
|
||||
text = _("Hello, {user_fist_name}!\n"
|
||||
"This is the example of multilanguage bot.\n"
|
||||
"Available commands:\n\n"
|
||||
"/lang - change your language\n"
|
||||
"/plural - pluralization example", lang=lang)
|
||||
"/plural - pluralization example")
|
||||
|
||||
# remember don't use f string for interpolation, use .format method instead
|
||||
text = text.format(user_fist_name=message.from_user.first_name)
|
||||
bot.send_message(message.from_user.id, text)
|
||||
|
||||
|
||||
@bot.message_handler(commands='lang')
|
||||
@bot.message_handler(commands=['lang'])
|
||||
def change_language_handler(message: types.Message):
|
||||
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
|
||||
reply_markup=keyboards.languages_keyboard())
|
||||
@@ -106,13 +90,13 @@ def language_handler(call: types.CallbackQuery):
|
||||
lang = call.data
|
||||
users_lang[call.from_user.id] = lang
|
||||
|
||||
# When you change user's language, pass language explicitly coz it's not changed in context
|
||||
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||
bot.delete_state(call.from_user.id)
|
||||
|
||||
|
||||
@bot.message_handler(commands='plural')
|
||||
@get_user_language
|
||||
def pluralization_handler(message: types.Message, lang):
|
||||
@bot.message_handler(commands=['plural'])
|
||||
def pluralization_handler(message: types.Message):
|
||||
if not users_clicks.get(message.from_user.id):
|
||||
users_clicks[message.from_user.id] = 0
|
||||
clicks = users_clicks[message.from_user.id]
|
||||
@@ -121,15 +105,13 @@ def pluralization_handler(message: types.Message, lang):
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks,
|
||||
lang=lang
|
||||
)
|
||||
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_, lang))
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
|
||||
@get_user_language
|
||||
def click_handler(call: types.CallbackQuery, lang):
|
||||
def click_handler(call: types.CallbackQuery):
|
||||
if not users_clicks.get(call.from_user.id):
|
||||
users_clicks[call.from_user.id] = 1
|
||||
else:
|
||||
@@ -140,12 +122,11 @@ def click_handler(call: types.CallbackQuery, lang):
|
||||
text = __(
|
||||
singular="You have {number} click",
|
||||
plural="You have {number} clicks",
|
||||
n=clicks,
|
||||
lang=lang
|
||||
n=clicks
|
||||
)
|
||||
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
|
||||
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||
reply_markup=keyboards.clicker_keyboard(_, lang))
|
||||
reply_markup=keyboards.clicker_keyboard(_))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
17
examples/multibot/README.MD
Normal file
17
examples/multibot/README.MD
Normal file
@@ -0,0 +1,17 @@
|
||||
You probably have seen bots which allow you to send them token of your bot and then handle updates providing some functionality for your bot.
|
||||
<br>
|
||||
This type of bots are called <b>multibots</b>. They are created using webhooks.
|
||||
<br>
|
||||
|
||||
This is the example of simple multibot.<br>
|
||||
In order to reproduce this example you need to have <b>domain and ssl connection</b>.
|
||||
<br>
|
||||
If you have, go to config.py and specify your data.
|
||||
<br>
|
||||
There is also file called <b>nginx_conf.conf</b>, we will use nginx as proxy-server and this file is example nginx.conf file.
|
||||
<br>
|
||||
Make sure that server_name and port are the same in both config and nginx_conf
|
||||
<br>
|
||||
(nginx_conf.conf IS NOT complete, you would probably use tools like certbot to add ssl connection to it)
|
||||
<br>
|
||||
Also, in this example I used dictionary as tokens storage, but in production you should use database so that you can re-set webhooks in case bot restarts.
|
||||
6
examples/multibot/config.py
Normal file
6
examples/multibot/config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
MAIN_BOT_TOKEN = "your_main_bot_token"
|
||||
|
||||
WEBHOOK_HOST = "your_domain.com"
|
||||
WEBHOOK_PATH = "telegram_webhook"
|
||||
WEBAPP_HOST = "0.0.0.0"
|
||||
WEBAPP_PORT = 3500
|
||||
14
examples/multibot/handlers.py
Normal file
14
examples/multibot/handlers.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from telebot import types, TeleBot
|
||||
|
||||
|
||||
def hello_handler(message: types.Message, bot: TeleBot):
|
||||
bot.send_message(message.chat.id, "Hi :)")
|
||||
|
||||
|
||||
def echo_handler(message: types.Message, bot: TeleBot):
|
||||
bot.send_message(message.chat.id, message.text)
|
||||
|
||||
|
||||
def register_handlers(bot: TeleBot):
|
||||
bot.register_message_handler(hello_handler, func=lambda message: message.text == 'Hello', pass_bot=True)
|
||||
bot.register_message_handler(echo_handler, pass_bot=True)
|
||||
48
examples/multibot/main.py
Normal file
48
examples/multibot/main.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from flask import Flask
|
||||
from flask import request, abort
|
||||
from telebot import TeleBot, types, util
|
||||
from handlers import register_handlers
|
||||
|
||||
import config
|
||||
|
||||
main_bot = TeleBot(config.MAIN_BOT_TOKEN)
|
||||
app = Flask(__name__)
|
||||
tokens = {config.MAIN_BOT_TOKEN: True}
|
||||
|
||||
|
||||
@app.route(f"/{config.WEBHOOK_PATH}/<token>", methods=['POST'])
|
||||
def webhook(token: str):
|
||||
if not tokens.get(token):
|
||||
return abort(404)
|
||||
|
||||
if request.headers.get('content-type') != 'application/json':
|
||||
return abort(403)
|
||||
|
||||
json_string = request.get_data().decode('utf-8')
|
||||
update = types.Update.de_json(json_string)
|
||||
if token == main_bot.token:
|
||||
main_bot.process_new_updates([update])
|
||||
return ''
|
||||
|
||||
from_update_bot = TeleBot(token)
|
||||
register_handlers(from_update_bot)
|
||||
from_update_bot.process_new_updates([update])
|
||||
return ''
|
||||
|
||||
|
||||
@main_bot.message_handler(commands=['add_bot'])
|
||||
def add_bot(message: types.Message):
|
||||
token = util.extract_arguments(message.text)
|
||||
tokens[token] = True
|
||||
|
||||
new_bot = TeleBot(token)
|
||||
new_bot.delete_webhook()
|
||||
new_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{token}")
|
||||
|
||||
new_bot.send_message(message.chat.id, "Webhook was set.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main_bot.delete_webhook()
|
||||
main_bot.set_webhook(f"{config.WEBHOOK_HOST}/{config.WEBHOOK_PATH}/{config.MAIN_BOT_TOKEN}")
|
||||
app.run(host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)
|
||||
8
examples/multibot/nginx_conf.conf
Normal file
8
examples/multibot/nginx_conf.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
server_name your_domain.com;
|
||||
|
||||
location /telegram_webhook/ {
|
||||
proxy_pass http://localhost:3500;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pytest
|
||||
requests==2.20.0
|
||||
wheel==0.24.0
|
||||
aiohttp>=3.8.0,<3.9.0
|
||||
aiohttp>=3.8.0,<3.9.0
|
||||
3
setup.py
3
setup.py
@@ -26,7 +26,8 @@ setup(name='pyTelegramBotAPI',
|
||||
extras_require={
|
||||
'json': 'ujson',
|
||||
'PIL': 'Pillow',
|
||||
'redis': 'redis>=3.4.1'
|
||||
'redis': 'redis>=3.4.1',
|
||||
'aiohttp': 'aiohttp',
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
||||
@@ -9,6 +9,7 @@ import time
|
||||
import traceback
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
|
||||
|
||||
# these imports are used to avoid circular import error
|
||||
import telebot.util
|
||||
import telebot.types
|
||||
@@ -187,8 +188,6 @@ class TeleBot:
|
||||
compatibility whose purpose was to enable file saving capability for handlers. And the same
|
||||
implementation is now available with FileHandlerBackend
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
|
||||
:param delay: Delay between changes in handlers and saving
|
||||
:param filename: Filename of save file
|
||||
"""
|
||||
@@ -211,8 +210,6 @@ class TeleBot:
|
||||
compatibility whose purpose was to enable file saving capability for handlers. And the same
|
||||
implementation is now available with FileHandlerBackend
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
|
||||
:param delay: Delay between changes in handlers and saving
|
||||
:param filename: Filename of save file
|
||||
"""
|
||||
@@ -225,8 +222,6 @@ class TeleBot:
|
||||
This function is left to keep backward compatibility whose purpose was to disable file saving capability
|
||||
for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new next_step_backend backend
|
||||
instead of FileHandlerBackend.
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
"""
|
||||
self.next_step_backend = MemoryHandlerBackend(self.next_step_backend.handlers)
|
||||
|
||||
@@ -237,8 +232,6 @@ class TeleBot:
|
||||
This function is left to keep backward compatibility whose purpose was to disable file saving capability
|
||||
for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new reply_backend backend
|
||||
instead of FileHandlerBackend.
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
"""
|
||||
self.reply_backend = MemoryHandlerBackend(self.reply_backend.handlers)
|
||||
|
||||
@@ -250,8 +243,6 @@ class TeleBot:
|
||||
help of FileHandlerBackend and is only recommended to use if next_step_backend was assigned as
|
||||
FileHandlerBackend before entering this function
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
|
||||
:param filename: Filename of the file where handlers was saved
|
||||
:param del_file_after_loading: Is passed True, after loading save file will be deleted
|
||||
"""
|
||||
@@ -265,15 +256,13 @@ class TeleBot:
|
||||
help of FileHandlerBackend and is only recommended to use if reply_backend was assigned as
|
||||
FileHandlerBackend before entering this function
|
||||
|
||||
Most probably this function should be deprecated in future major releases
|
||||
|
||||
:param filename: Filename of the file where handlers was saved
|
||||
:param del_file_after_loading: Is passed True, after loading save file will be deleted
|
||||
"""
|
||||
self.reply_backend.load_handlers(filename, del_file_after_loading)
|
||||
|
||||
def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
|
||||
drop_pending_updates = None, timeout=None):
|
||||
drop_pending_updates = None, timeout=None, secret_token=None):
|
||||
"""
|
||||
Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an
|
||||
update for the bot, we will send an HTTPS POST request to the specified url,
|
||||
@@ -298,10 +287,11 @@ class TeleBot:
|
||||
resolved through DNS
|
||||
:param drop_pending_updates: Pass True to drop all pending updates
|
||||
:param timeout: Integer. Request connection timeout
|
||||
:param secret_token: Secret token to be used to verify the webhook request.
|
||||
:return: API reply.
|
||||
"""
|
||||
return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address,
|
||||
drop_pending_updates, timeout)
|
||||
drop_pending_updates, timeout, secret_token)
|
||||
|
||||
def delete_webhook(self, drop_pending_updates=None, timeout=None):
|
||||
"""
|
||||
@@ -396,7 +386,7 @@ class TeleBot:
|
||||
new_poll_answers = None
|
||||
new_my_chat_members = None
|
||||
new_chat_members = None
|
||||
chat_join_request = None
|
||||
new_chat_join_request = None
|
||||
|
||||
for update in updates:
|
||||
if apihelper.ENABLE_MIDDLEWARE:
|
||||
@@ -453,8 +443,8 @@ class TeleBot:
|
||||
if new_chat_members is None: new_chat_members = []
|
||||
new_chat_members.append(update.chat_member)
|
||||
if update.chat_join_request:
|
||||
if chat_join_request is None: chat_join_request = []
|
||||
chat_join_request.append(update.chat_join_request)
|
||||
if new_chat_join_request is None: new_chat_join_request = []
|
||||
new_chat_join_request.append(update.chat_join_request)
|
||||
|
||||
if new_messages:
|
||||
self.process_new_messages(new_messages)
|
||||
@@ -482,8 +472,8 @@ class TeleBot:
|
||||
self.process_new_my_chat_member(new_my_chat_members)
|
||||
if new_chat_members:
|
||||
self.process_new_chat_member(new_chat_members)
|
||||
if chat_join_request:
|
||||
self.process_new_chat_join_request(chat_join_request)
|
||||
if new_chat_join_request:
|
||||
self.process_new_chat_join_request(new_chat_join_request)
|
||||
|
||||
def process_new_messages(self, new_messages):
|
||||
self._notify_next_handlers(new_messages)
|
||||
@@ -554,15 +544,15 @@ class TeleBot:
|
||||
for listener in self.update_listener:
|
||||
self._exec_task(listener, new_messages)
|
||||
|
||||
def infinity_polling(self, timeout: int=20, skip_pending: bool=False, long_polling_timeout: int=20, logger_level=logging.ERROR,
|
||||
allowed_updates: Optional[List[str]]=None, *args, **kwargs):
|
||||
def infinity_polling(self, timeout: int=20, skip_pending: bool=False, long_polling_timeout: int=20,
|
||||
logger_level=logging.ERROR, allowed_updates: Optional[List[str]]=None, *args, **kwargs):
|
||||
"""
|
||||
Wrap polling with infinite loop and exception handling to avoid bot stops polling.
|
||||
|
||||
:param timeout: Request connection timeout
|
||||
:param long_polling_timeout: Timeout in seconds for long polling (see API docs)
|
||||
:param skip_pending: skip old updates
|
||||
:param logger_level: Custom logging level for infinity_polling logging.
|
||||
:param logger_level: Custom (different from logger itself) logging level for infinity_polling logging.
|
||||
Use logger levels from logging as a value. None/NOTSET = no error logging
|
||||
:param allowed_updates: A list of the update types you want your bot to receive.
|
||||
For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types.
|
||||
@@ -578,8 +568,8 @@ class TeleBot:
|
||||
|
||||
while not self.__stop_polling.is_set():
|
||||
try:
|
||||
self.polling(none_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout,
|
||||
allowed_updates=allowed_updates, *args, **kwargs)
|
||||
self.polling(non_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout,
|
||||
logger_level=logger_level, allowed_updates=allowed_updates, *args, **kwargs)
|
||||
except Exception as e:
|
||||
if logger_level and logger_level >= logging.ERROR:
|
||||
logger.error("Infinity polling exception: %s", str(e))
|
||||
@@ -592,9 +582,9 @@ class TeleBot:
|
||||
if logger_level and logger_level >= logging.INFO:
|
||||
logger.error("Break infinity polling")
|
||||
|
||||
def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20,
|
||||
long_polling_timeout: int=20, allowed_updates: Optional[List[str]]=None,
|
||||
none_stop: Optional[bool]=None):
|
||||
def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20, long_polling_timeout: int=20,
|
||||
logger_level=logging.ERROR, allowed_updates: Optional[List[str]]=None,
|
||||
none_stop: Optional[bool]=None):
|
||||
"""
|
||||
This function creates a new Thread that calls an internal __retrieve_updates function.
|
||||
This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly.
|
||||
@@ -608,6 +598,8 @@ class TeleBot:
|
||||
:param timeout: Request connection timeout
|
||||
:param skip_pending: skip old updates
|
||||
:param long_polling_timeout: Timeout in seconds for long polling (see API docs)
|
||||
:param logger_level: Custom (different from logger itself) logging level for infinity_polling logging.
|
||||
Use logger levels from logging as a value. None/NOTSET = no error logging
|
||||
:param allowed_updates: A list of the update types you want your bot to receive.
|
||||
For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types.
|
||||
See util.update_types for a complete list of available update types.
|
||||
@@ -620,18 +612,27 @@ class TeleBot:
|
||||
:return:
|
||||
"""
|
||||
if none_stop is not None:
|
||||
logger.warning("polling: none_stop parameter is deprecated. Use non_stop instead.")
|
||||
non_stop = none_stop
|
||||
|
||||
if skip_pending:
|
||||
self.__skip_updates()
|
||||
|
||||
if self.threaded:
|
||||
self.__threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates)
|
||||
self.__threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout,
|
||||
logger_level=logger_level, allowed_updates=allowed_updates)
|
||||
else:
|
||||
self.__non_threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates)
|
||||
self.__non_threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout,
|
||||
logger_level=logger_level, allowed_updates=allowed_updates)
|
||||
|
||||
def __threaded_polling(self, non_stop=False, interval=0, timeout = None, long_polling_timeout = None, allowed_updates=None):
|
||||
logger.info('Started polling.')
|
||||
def __threaded_polling(self, non_stop = False, interval = 0, timeout = None, long_polling_timeout = None,
|
||||
logger_level=logging.ERROR, allowed_updates=None):
|
||||
if not(logger_level) or (logger_level < logging.INFO):
|
||||
warning = "\n Warning: this message appearance will be changed. Set logger_level=logging.INFO to continue seeing it."
|
||||
else:
|
||||
warning = ""
|
||||
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info('Started polling.' + warning)
|
||||
self.__stop_polling.clear()
|
||||
error_interval = 0.25
|
||||
|
||||
@@ -656,14 +657,17 @@ class TeleBot:
|
||||
else:
|
||||
handled = False
|
||||
if not handled:
|
||||
logger.error(e)
|
||||
if logger_level and logger_level >= logging.ERROR:
|
||||
logger.error("Threaded polling exception: %s", str(e))
|
||||
if logger_level and logger_level >= logging.DEBUG:
|
||||
logger.error("Exception traceback:\n%s", traceback.format_exc())
|
||||
if not non_stop:
|
||||
self.__stop_polling.set()
|
||||
logger.info("Exception occurred. Stopping.")
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("Exception occurred. Stopping." + warning)
|
||||
else:
|
||||
# polling_thread.clear_exceptions()
|
||||
# self.worker_pool.clear_exceptions()
|
||||
logger.info("Waiting for {0} seconds until retry".format(error_interval))
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("Waiting for {0} seconds until retry".format(error_interval) + warning)
|
||||
time.sleep(error_interval)
|
||||
if error_interval * 2 < 60:
|
||||
error_interval *= 2
|
||||
@@ -676,7 +680,8 @@ class TeleBot:
|
||||
polling_thread.clear_exceptions() #*
|
||||
self.worker_pool.clear_exceptions() #*
|
||||
except KeyboardInterrupt:
|
||||
logger.info("KeyboardInterrupt received.")
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("KeyboardInterrupt received." + warning)
|
||||
self.__stop_polling.set()
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -695,12 +700,19 @@ class TeleBot:
|
||||
time.sleep(error_interval)
|
||||
|
||||
polling_thread.stop()
|
||||
polling_thread.clear_exceptions() #*
|
||||
self.worker_pool.clear_exceptions() #*
|
||||
logger.info('Stopped polling.')
|
||||
polling_thread.clear_exceptions()
|
||||
self.worker_pool.clear_exceptions()
|
||||
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info('Stopped polling.' + warning)
|
||||
|
||||
def __non_threaded_polling(self, non_stop=False, interval=0, timeout=None, long_polling_timeout=None, allowed_updates=None):
|
||||
logger.info('Started polling.')
|
||||
def __non_threaded_polling(self, non_stop=False, interval=0, timeout=None, long_polling_timeout=None,
|
||||
logger_level=logging.ERROR, allowed_updates=None):
|
||||
if not(logger_level) or (logger_level < logging.INFO):
|
||||
warning = "\n Warning: this message appearance will be changed. Set logger_level=logging.INFO to continue seeing it."
|
||||
else:
|
||||
warning = ""
|
||||
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info('Started polling.' + warning)
|
||||
self.__stop_polling.clear()
|
||||
error_interval = 0.25
|
||||
|
||||
@@ -715,18 +727,24 @@ class TeleBot:
|
||||
handled = False
|
||||
|
||||
if not handled:
|
||||
logger.error(e)
|
||||
if logger_level and logger_level >= logging.ERROR:
|
||||
logger.error("Polling exception: %s", str(e))
|
||||
if logger_level and logger_level >= logging.DEBUG:
|
||||
logger.error("Exception traceback:\n%s", traceback.format_exc())
|
||||
if not non_stop:
|
||||
self.__stop_polling.set()
|
||||
logger.info("Exception occurred. Stopping.")
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("Exception occurred. Stopping." + warning)
|
||||
else:
|
||||
logger.info("Waiting for {0} seconds until retry".format(error_interval))
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("Waiting for {0} seconds until retry".format(error_interval) + warning)
|
||||
time.sleep(error_interval)
|
||||
error_interval *= 2
|
||||
else:
|
||||
time.sleep(error_interval)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("KeyboardInterrupt received.")
|
||||
# if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info("KeyboardInterrupt received." + warning)
|
||||
self.__stop_polling.set()
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -738,8 +756,8 @@ class TeleBot:
|
||||
raise e
|
||||
else:
|
||||
time.sleep(error_interval)
|
||||
|
||||
logger.info('Stopped polling.')
|
||||
#if logger_level and logger_level >= logging.INFO: # enable in future releases. Change output to logger.error
|
||||
logger.info('Stopped polling.' + warning)
|
||||
|
||||
def _exec_task(self, task, *args, **kwargs):
|
||||
if kwargs and kwargs.get('task_type') == 'handler':
|
||||
@@ -887,11 +905,11 @@ class TeleBot:
|
||||
result = apihelper.get_chat_administrators(self.token, chat_id)
|
||||
return [types.ChatMember.de_json(r) for r in result]
|
||||
|
||||
@util.deprecated(deprecation_text="Use get_chat_member_count instead")
|
||||
def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
|
||||
"""
|
||||
This function is deprecated. Use `get_chat_member_count` instead
|
||||
"""
|
||||
logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.')
|
||||
result = apihelper.get_chat_member_count(self.token, chat_id)
|
||||
return result
|
||||
|
||||
@@ -1024,7 +1042,7 @@ class TeleBot:
|
||||
reply_to_message_id: Optional[int]=None,
|
||||
allow_sending_without_reply: Optional[bool]=None,
|
||||
reply_markup: Optional[REPLY_MARKUP_TYPES]=None,
|
||||
timeout: Optional[int]=None) -> int:
|
||||
timeout: Optional[int]=None) -> types.MessageID:
|
||||
"""
|
||||
Use this method to copy messages of any kind.
|
||||
|
||||
@@ -1662,6 +1680,7 @@ class TeleBot:
|
||||
"""
|
||||
return apihelper.send_chat_action(self.token, chat_id, action, timeout)
|
||||
|
||||
@util.deprecated(deprecation_text="Use ban_chat_member instead")
|
||||
def kick_chat_member(
|
||||
self, chat_id: Union[int, str], user_id: int,
|
||||
until_date:Optional[Union[int, datetime]]=None,
|
||||
@@ -1669,7 +1688,6 @@ class TeleBot:
|
||||
"""
|
||||
This function is deprecated. Use `ban_chat_member` instead
|
||||
"""
|
||||
logger.info('kick_chat_member is deprecated. Use ban_chat_member instead.')
|
||||
return apihelper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages)
|
||||
|
||||
def ban_chat_member(
|
||||
@@ -1772,6 +1790,7 @@ class TeleBot:
|
||||
can_promote_members: Optional[bool]=None,
|
||||
is_anonymous: Optional[bool]=None,
|
||||
can_manage_chat: Optional[bool]=None,
|
||||
can_manage_video_chats: Optional[bool]=None,
|
||||
can_manage_voice_chats: Optional[bool]=None) -> bool:
|
||||
"""
|
||||
Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator
|
||||
@@ -1798,15 +1817,22 @@ class TeleBot:
|
||||
message statistics in channels, see channel members,
|
||||
see anonymous administrators in supergroups and ignore slow mode.
|
||||
Implied by any other administrator privilege
|
||||
:param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats
|
||||
:param can_manage_video_chats: Bool: Pass True, if the administrator can manage voice chats
|
||||
For now, bots can use this privilege only for passing to other administrators.
|
||||
:param can_manage_voice_chats: Deprecated, use can_manage_video_chats.
|
||||
|
||||
:return: True on success.
|
||||
"""
|
||||
if can_manage_voice_chats is not None:
|
||||
logger.warning("promote_chat_member: can_manage_voice_chats parameter is deprecated. Use can_manage_video_chats instead.")
|
||||
if can_manage_video_chats is None:
|
||||
can_manage_video_chats = can_manage_voice_chats
|
||||
|
||||
return apihelper.promote_chat_member(
|
||||
self.token, chat_id, user_id, can_change_info, can_post_messages,
|
||||
can_edit_messages, can_delete_messages, can_invite_users,
|
||||
can_restrict_members, can_pin_messages, can_promote_members,
|
||||
is_anonymous, can_manage_chat, can_manage_voice_chats)
|
||||
is_anonymous, can_manage_chat, can_manage_video_chats)
|
||||
|
||||
def set_chat_administrator_custom_title(
|
||||
self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool:
|
||||
@@ -2033,6 +2059,71 @@ class TeleBot:
|
||||
result = apihelper.get_my_commands(self.token, scope, language_code)
|
||||
return [types.BotCommand.de_json(cmd) for cmd in result]
|
||||
|
||||
def set_chat_menu_button(self, chat_id: Union[int, str]=None,
|
||||
menu_button: types.MenuButton=None) -> bool:
|
||||
"""
|
||||
Use this method to change the bot's menu button in a private chat,
|
||||
or the default menu button.
|
||||
Returns True on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#setchatmenubutton
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat.
|
||||
If not specified, default bot's menu button will be changed.
|
||||
:param menu_button: A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault
|
||||
|
||||
"""
|
||||
return apihelper.set_chat_menu_button(self.token, chat_id, menu_button)
|
||||
|
||||
|
||||
def get_chat_menu_button(self, chat_id: Union[int, str]=None) -> types.MenuButton:
|
||||
"""
|
||||
Use this method to get the current value of the bot's menu button
|
||||
in a private chat, or the default menu button.
|
||||
Returns MenuButton on success.
|
||||
|
||||
Telegram Documentation: https://core.telegram.org/bots/api#getchatmenubutton
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat.
|
||||
If not specified, default bot's menu button will be returned.
|
||||
:return: types.MenuButton
|
||||
|
||||
"""
|
||||
return types.MenuButton.de_json(apihelper.get_chat_menu_button(self.token, chat_id))
|
||||
|
||||
|
||||
def set_my_default_administrator_rights(self, rights: types.ChatAdministratorRights=None,
|
||||
for_channels: bool=None) -> bool:
|
||||
"""
|
||||
Use this method to change the default administrator rights requested by the bot
|
||||
when it's added as an administrator to groups or channels.
|
||||
These rights will be suggested to users, but they are are free to modify
|
||||
the list before adding the bot.
|
||||
Returns True on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#setmydefaultadministratorrights
|
||||
|
||||
:param rights: A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared.
|
||||
:param for_channels: Pass True to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed.
|
||||
"""
|
||||
|
||||
return apihelper.set_my_default_administrator_rights(self.token, rights, for_channels)
|
||||
|
||||
|
||||
def get_my_default_administrator_rights(self, for_channels: bool=None) -> types.ChatAdministratorRights:
|
||||
"""
|
||||
Use this method to get the current default administrator rights of the bot.
|
||||
Returns ChatAdministratorRights on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#getmydefaultadministratorrights
|
||||
|
||||
:param for_channels: Pass True to get the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be returned.
|
||||
:return: types.ChatAdministratorRights
|
||||
"""
|
||||
|
||||
return types.ChatAdministratorRights.de_json(apihelper.get_my_default_administrator_rights(self.token, for_channels))
|
||||
|
||||
|
||||
def set_my_commands(self, commands: List[types.BotCommand],
|
||||
scope: Optional[types.BotCommandScope]=None,
|
||||
language_code: Optional[str]=None) -> bool:
|
||||
@@ -2373,6 +2464,69 @@ class TeleBot:
|
||||
max_tip_amount, suggested_tip_amounts, protect_content)
|
||||
return types.Message.de_json(result)
|
||||
|
||||
|
||||
def create_invoice_link(self,
|
||||
title: str, description: str, payload:str, provider_token: str,
|
||||
currency: str, prices: List[types.LabeledPrice],
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[List[int]]=None,
|
||||
provider_data: Optional[str]=None,
|
||||
photo_url: Optional[str]=None,
|
||||
photo_size: Optional[int]=None,
|
||||
photo_width: Optional[int]=None,
|
||||
photo_height: Optional[int]=None,
|
||||
need_name: Optional[bool]=None,
|
||||
need_phone_number: Optional[bool]=None,
|
||||
need_email: Optional[bool]=None,
|
||||
need_shipping_address: Optional[bool]=None,
|
||||
send_phone_number_to_provider: Optional[bool]=None,
|
||||
send_email_to_provider: Optional[bool]=None,
|
||||
is_flexible: Optional[bool]=None) -> str:
|
||||
|
||||
"""
|
||||
Use this method to create a link for an invoice.
|
||||
Returns the created invoice link as String on success.
|
||||
|
||||
Telegram documentation:
|
||||
https://core.telegram.org/bots/api#createinvoicelink
|
||||
|
||||
:param title: Product name, 1-32 characters
|
||||
:param description: Product description, 1-255 characters
|
||||
:param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user,
|
||||
use for your internal processes.
|
||||
:param provider_token: Payments provider token, obtained via @Botfather
|
||||
:param currency: Three-letter ISO 4217 currency code,
|
||||
see https://core.telegram.org/bots/payments#supported-currencies
|
||||
:param prices: Price breakdown, a list of components
|
||||
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
|
||||
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
|
||||
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest
|
||||
:param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider.
|
||||
A detailed description of required fields should be provided by the payment provider.
|
||||
:param photo_url: URL of the product photo for the invoice. Can be a photo of the goods
|
||||
:param photo_size: Photo size in bytes
|
||||
:param photo_width: Photo width
|
||||
:param photo_height: Photo height
|
||||
:param need_name: Pass True, if you require the user's full name to complete the order
|
||||
:param need_phone_number: Pass True, if you require the user's phone number to complete the order
|
||||
:param need_email: Pass True, if you require the user's email to complete the order
|
||||
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
|
||||
:param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
|
||||
:param send_email_to_provider: Pass True, if user's email address should be sent to provider
|
||||
:param is_flexible: Pass True, if the final price depends on the shipping method
|
||||
|
||||
:return: Created invoice link as String on success.
|
||||
"""
|
||||
result = apihelper.create_invoice_link(
|
||||
self.token, title, description, payload, provider_token,
|
||||
currency, prices, max_tip_amount, suggested_tip_amounts, provider_data,
|
||||
photo_url, photo_size, photo_width, photo_height, need_name, need_phone_number,
|
||||
need_email, need_shipping_address, send_phone_number_to_provider,
|
||||
send_email_to_provider, is_flexible)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
# TODO: rewrite this method like in API
|
||||
def send_poll(
|
||||
@@ -2421,6 +2575,8 @@ class TeleBot:
|
||||
if isinstance(question, types.Poll):
|
||||
raise RuntimeError("The send_poll signature was changed, please see send_poll function details.")
|
||||
|
||||
explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode
|
||||
|
||||
return types.Message.de_json(
|
||||
apihelper.send_poll(
|
||||
self.token, chat_id,
|
||||
@@ -2684,6 +2840,22 @@ class TeleBot:
|
||||
"""
|
||||
return apihelper.delete_sticker_from_set(self.token, sticker)
|
||||
|
||||
def answer_web_app_query(self, web_app_query_id: str, result: types.InlineQueryResultBase) -> types.SentWebAppMessage:
|
||||
"""
|
||||
Use this method to set the result of an interaction with a Web App and
|
||||
send a corresponding message on behalf of the user to the chat from which
|
||||
the query originated.
|
||||
On success, a SentWebAppMessage object is returned.
|
||||
|
||||
Telegram Documentation: https://core.telegram.org/bots/api#answerwebappquery
|
||||
|
||||
:param web_app_query_id: Unique identifier for the query to be answered
|
||||
:param result: A JSON-serialized object describing the message to be sent
|
||||
:return:
|
||||
"""
|
||||
|
||||
return apihelper.answer_web_app_query(self.token, web_app_query_id, result)
|
||||
|
||||
def register_for_reply(
|
||||
self, message: types.Message, callback: Callable, *args, **kwargs) -> None:
|
||||
"""
|
||||
@@ -3798,7 +3970,7 @@ class TeleBot:
|
||||
"""
|
||||
Check middleware
|
||||
|
||||
:param message:
|
||||
:param update_type:
|
||||
:return:
|
||||
"""
|
||||
middlewares = None
|
||||
@@ -3806,7 +3978,7 @@ class TeleBot:
|
||||
middlewares = [i for i in self.middlewares if update_type in i.update_types]
|
||||
return middlewares
|
||||
|
||||
def _run_middlewares_and_handler(self, message, handlers, middlewares, *args, **kwargs):
|
||||
def _run_middlewares_and_handler(self, message, handlers, middlewares):
|
||||
"""
|
||||
This class is made to run handler and middleware in queue.
|
||||
|
||||
@@ -3827,6 +3999,7 @@ class TeleBot:
|
||||
return
|
||||
elif isinstance(result, SkipHandler) and skip_handler is False:
|
||||
skip_handler = True
|
||||
|
||||
|
||||
try:
|
||||
if handlers and not skip_handler:
|
||||
@@ -3838,24 +4011,31 @@ class TeleBot:
|
||||
params.append(i)
|
||||
if len(params) == 1:
|
||||
handler['function'](message)
|
||||
|
||||
elif len(params) == 2:
|
||||
if handler.get('pass_bot') is True:
|
||||
handler['function'](message, self)
|
||||
|
||||
elif handler.get('pass_bot') is False:
|
||||
handler['function'](message, data)
|
||||
|
||||
elif len(params) == 3:
|
||||
if params[2] == 'bot' and handler.get('pass_bot') is True:
|
||||
handler['function'](message, data, self)
|
||||
else:
|
||||
if "data" in params:
|
||||
if len(params) == 2:
|
||||
handler['function'](message, data)
|
||||
elif len(params) == 3:
|
||||
handler['function'](message, data=data, bot=self)
|
||||
else:
|
||||
logger.error("It is not allowed to pass data and values inside data to the handler. Check your handler: {}".format(handler['function']))
|
||||
return
|
||||
|
||||
elif not handler.get('pass_bot'):
|
||||
raise RuntimeError('Your handler accepts 3 parameters but pass_bot is False. Please re-check your handler.')
|
||||
|
||||
else:
|
||||
handler['function'](message, self, data)
|
||||
|
||||
|
||||
data_copy = data.copy()
|
||||
|
||||
for key in list(data_copy):
|
||||
# remove data from data_copy if handler does not accept it
|
||||
if key not in params:
|
||||
del data_copy[key]
|
||||
if handler.get('pass_bot'): data_copy["bot"] = self
|
||||
if len(data_copy) > len(params) - 1: # remove the message parameter
|
||||
logger.error("You are passing more data than the handler needs. Check your handler: {}".format(handler['function']))
|
||||
return
|
||||
|
||||
handler["function"](message, **data_copy)
|
||||
|
||||
except Exception as e:
|
||||
handler_error = e
|
||||
|
||||
@@ -3864,6 +4044,7 @@ class TeleBot:
|
||||
return self.exception_handler.handle(e)
|
||||
logging.error(str(e))
|
||||
return
|
||||
# remove the bot from data
|
||||
if middlewares:
|
||||
for middleware in middlewares:
|
||||
middleware.post_process(message, data, handler_error)
|
||||
|
||||
@@ -47,7 +47,6 @@ CUSTOM_REQUEST_SENDER = None
|
||||
ENABLE_MIDDLEWARE = False
|
||||
|
||||
|
||||
|
||||
def _get_req_session(reset=False):
|
||||
if SESSION_TIME_TO_LIVE:
|
||||
# If session TTL is set - check time passed
|
||||
@@ -94,20 +93,14 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
||||
if 'timeout' in params:
|
||||
read_timeout = params.pop('timeout')
|
||||
connect_timeout = read_timeout
|
||||
# if 'connect-timeout' in params:
|
||||
# connect_timeout = params.pop('connect-timeout') + 10
|
||||
if 'long_polling_timeout' in params:
|
||||
# For getUpdates: it's the only function with timeout parameter on the BOT API side
|
||||
# For getUpdates. It's the only function with timeout parameter on the BOT API side
|
||||
long_polling_timeout = params.pop('long_polling_timeout')
|
||||
params['timeout'] = long_polling_timeout
|
||||
# Long polling hangs for a given time. Read timeout should be greater that long_polling_timeout
|
||||
read_timeout = max(long_polling_timeout + 5, read_timeout)
|
||||
# Lets stop suppose that user is stupid and assume that he knows what he do...
|
||||
# read_timeout = read_timeout + 10
|
||||
# connect_timeout = connect_timeout + 10
|
||||
|
||||
params = params or None # Set params to None if empty
|
||||
|
||||
result = None
|
||||
if RETRY_ON_ERROR and RETRY_ENGINE == 1:
|
||||
got_result = False
|
||||
@@ -134,6 +127,7 @@ def _make_request(token, method_name, method='get', params=None, files=None):
|
||||
timeout=(connect_timeout, read_timeout), proxies=proxy)
|
||||
elif RETRY_ON_ERROR and RETRY_ENGINE == 2:
|
||||
http = _get_req_session()
|
||||
# noinspection PyUnresolvedReferences
|
||||
retry_strategy = requests.packages.urllib3.util.retry.Retry(
|
||||
total=MAX_RETRIES,
|
||||
)
|
||||
@@ -274,7 +268,7 @@ def send_message(
|
||||
|
||||
|
||||
def set_webhook(token, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
|
||||
drop_pending_updates = None, timeout=None):
|
||||
drop_pending_updates = None, timeout=None, secret_token=None):
|
||||
method_url = r'setWebhook'
|
||||
payload = {
|
||||
'url': url if url else "",
|
||||
@@ -292,6 +286,8 @@ def set_webhook(token, url=None, certificate=None, max_connections=None, allowed
|
||||
payload['drop_pending_updates'] = drop_pending_updates
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if secret_token:
|
||||
payload['secret_token'] = secret_token
|
||||
return _make_request(token, method_url, params=payload, files=files)
|
||||
|
||||
|
||||
@@ -971,7 +967,7 @@ def promote_chat_member(
|
||||
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
|
||||
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
|
||||
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
|
||||
method_url = 'promoteChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
if can_change_info is not None:
|
||||
@@ -994,8 +990,8 @@ def promote_chat_member(
|
||||
payload['is_anonymous'] = is_anonymous
|
||||
if can_manage_chat is not None:
|
||||
payload['can_manage_chat'] = can_manage_chat
|
||||
if can_manage_voice_chats is not None:
|
||||
payload['can_manage_voice_chats'] = can_manage_voice_chats
|
||||
if can_manage_video_chats is not None:
|
||||
payload['can_manage_video_chats'] = can_manage_video_chats
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
@@ -1139,6 +1135,40 @@ def get_my_commands(token, scope=None, language_code=None):
|
||||
payload['language_code'] = language_code
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
def set_chat_menu_button(token, chat_id=None, menu_button=None):
|
||||
method_url = r'setChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
if menu_button:
|
||||
payload['menu_button'] = menu_button.to_json()
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
def get_chat_menu_button(token, chat_id=None):
|
||||
method_url = r'getChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def set_my_default_administrator_rights(token, rights=None, for_channels=None):
|
||||
method_url = r'setMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if rights:
|
||||
payload['rights'] = rights.to_json()
|
||||
if for_channels is not None:
|
||||
payload['for_channels'] = for_channels
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def get_my_default_administrator_rights(token, for_channels=None):
|
||||
method_url = r'getMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if for_channels:
|
||||
payload['for_channels'] = for_channels
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def set_my_commands(token, commands, scope=None, language_code=None):
|
||||
method_url = r'setMyCommands'
|
||||
@@ -1289,7 +1319,6 @@ def send_game(
|
||||
payload['allow_sending_without_reply'] = allow_sending_without_reply
|
||||
if protect_content is not None:
|
||||
payload['protect_content'] = protect_content
|
||||
|
||||
return _make_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
@@ -1536,7 +1565,6 @@ def create_new_sticker_set(
|
||||
contains_masks=None, mask_position=None, webm_sticker=None):
|
||||
method_url = 'createNewStickerSet'
|
||||
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
|
||||
stype = None
|
||||
if png_sticker:
|
||||
stype = 'png_sticker'
|
||||
elif webm_sticker:
|
||||
@@ -1561,7 +1589,6 @@ def create_new_sticker_set(
|
||||
def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker):
|
||||
method_url = 'addStickerToSet'
|
||||
payload = {'user_id': user_id, 'name': name, 'emojis': emojis}
|
||||
stype = None
|
||||
if png_sticker:
|
||||
stype = 'png_sticker'
|
||||
elif webm_sticker:
|
||||
@@ -1591,6 +1618,51 @@ def delete_sticker_from_set(token, sticker):
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
|
||||
method_url = 'answerWebAppQuery'
|
||||
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
def create_invoice_link(token, title, description, payload, provider_token,
|
||||
currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None,
|
||||
photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None,
|
||||
need_email=None, need_shipping_address=None, send_phone_number_to_provider=None,
|
||||
send_email_to_provider=None, is_flexible=None):
|
||||
method_url = r'createInvoiceLink'
|
||||
payload = {'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token,
|
||||
'currency': currency, 'prices': _convert_list_json_serializable(prices)}
|
||||
if max_tip_amount:
|
||||
payload['max_tip_amount'] = max_tip_amount
|
||||
if suggested_tip_amounts:
|
||||
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
|
||||
if provider_data:
|
||||
payload['provider_data'] = provider_data
|
||||
if photo_url:
|
||||
payload['photo_url'] = photo_url
|
||||
if photo_size:
|
||||
payload['photo_size'] = photo_size
|
||||
if photo_width:
|
||||
payload['photo_width'] = photo_width
|
||||
if photo_height:
|
||||
payload['photo_height'] = photo_height
|
||||
if need_name is not None:
|
||||
payload['need_name'] = need_name
|
||||
if need_phone_number is not None:
|
||||
payload['need_phone_number'] = need_phone_number
|
||||
if need_email is not None:
|
||||
payload['need_email'] = need_email
|
||||
if need_shipping_address is not None:
|
||||
payload['need_shipping_address'] = need_shipping_address
|
||||
if send_phone_number_to_provider is not None:
|
||||
payload['send_phone_number_to_provider'] = send_phone_number_to_provider
|
||||
if send_email_to_provider is not None:
|
||||
payload['send_email_to_provider'] = send_email_to_provider
|
||||
if is_flexible is not None:
|
||||
payload['is_flexible'] = is_flexible
|
||||
return _make_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def send_poll(
|
||||
token, chat_id,
|
||||
@@ -1735,7 +1807,8 @@ class ApiException(Exception):
|
||||
super(ApiException, self).__init__("A request to the Telegram API was unsuccessful. {0}".format(msg))
|
||||
self.function_name = function_name
|
||||
self.result = result
|
||||
|
||||
|
||||
|
||||
class ApiHTTPException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a call to the
|
||||
@@ -1747,7 +1820,8 @@ class ApiHTTPException(ApiException):
|
||||
.format(result.status_code, result.reason, result.text.encode('utf8')),
|
||||
function_name,
|
||||
result)
|
||||
|
||||
|
||||
|
||||
class ApiInvalidJSONException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a call to the
|
||||
@@ -1759,7 +1833,8 @@ class ApiInvalidJSONException(ApiException):
|
||||
.format(result.text.encode('utf8')),
|
||||
function_name,
|
||||
result)
|
||||
|
||||
|
||||
|
||||
class ApiTelegramException(ApiException):
|
||||
"""
|
||||
This class represents an Exception thrown when a Telegram API returns error code.
|
||||
@@ -1773,4 +1848,3 @@ class ApiTelegramException(ApiException):
|
||||
self.result_json = result_json
|
||||
self.error_code = result_json['error_code']
|
||||
self.description = result_json['description']
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ class AsyncTeleBot:
|
||||
:return:
|
||||
"""
|
||||
if none_stop is not None:
|
||||
logger.warning("polling: none_stop parameter is deprecated. Use non_stop instead.")
|
||||
non_stop = none_stop
|
||||
|
||||
if skip_pending:
|
||||
@@ -276,7 +277,7 @@ class AsyncTeleBot:
|
||||
handler_error = None
|
||||
data = {}
|
||||
process_handler = True
|
||||
|
||||
params = []
|
||||
if middlewares:
|
||||
for middleware in middlewares:
|
||||
middleware_result = await middleware.pre_process(message, data)
|
||||
@@ -294,27 +295,34 @@ class AsyncTeleBot:
|
||||
continue
|
||||
elif process_update:
|
||||
try:
|
||||
params = []
|
||||
|
||||
for i in signature(handler['function']).parameters:
|
||||
params.append(i)
|
||||
if len(params) == 1:
|
||||
await handler['function'](message)
|
||||
break
|
||||
elif len(params) == 2:
|
||||
if handler['pass_bot']:
|
||||
await handler['function'](message, self)
|
||||
break
|
||||
else:
|
||||
await handler['function'](message, data)
|
||||
break
|
||||
elif len(params) == 3:
|
||||
if handler['pass_bot'] and params[1] == 'bot':
|
||||
await handler['function'](message, self, data)
|
||||
break
|
||||
else:
|
||||
await handler['function'](message, data)
|
||||
break
|
||||
else:
|
||||
if "data" in params:
|
||||
if len(params) == 2:
|
||||
await handler['function'](message, data)
|
||||
elif len(params) == 3:
|
||||
await handler['function'](message, data=data, bot=self)
|
||||
else:
|
||||
logger.error("It is not allowed to pass data and values inside data to the handler. Check your handler: {}".format(handler['function']))
|
||||
return
|
||||
|
||||
else:
|
||||
|
||||
data_copy = data.copy()
|
||||
|
||||
for key in list(data_copy):
|
||||
# remove data from data_copy if handler does not accept it
|
||||
if key not in params:
|
||||
del data_copy[key]
|
||||
if handler.get('pass_bot'): data_copy["bot"] = self
|
||||
if len(data_copy) > len(params) - 1: # remove the message parameter
|
||||
logger.error("You are passing more data than the handler needs. Check your handler: {}".format(handler['function']))
|
||||
return
|
||||
|
||||
await handler["function"](message, **data_copy)
|
||||
except Exception as e:
|
||||
handler_error = e
|
||||
|
||||
@@ -1383,7 +1391,7 @@ class AsyncTeleBot:
|
||||
self.current_states = StatePickleStorage(file_path=filename)
|
||||
|
||||
async def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
|
||||
drop_pending_updates = None, timeout=None):
|
||||
drop_pending_updates = None, timeout=None, secret_token=None):
|
||||
"""
|
||||
Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an
|
||||
update for the bot, we will send an HTTPS POST request to the specified url,
|
||||
@@ -1408,10 +1416,11 @@ class AsyncTeleBot:
|
||||
resolved through DNS
|
||||
:param drop_pending_updates: Pass True to drop all pending updates
|
||||
:param timeout: Integer. Request connection timeout
|
||||
:param secret_token: Secret token to be used to verify the webhook
|
||||
:return:
|
||||
"""
|
||||
return await asyncio_helper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address,
|
||||
drop_pending_updates, timeout)
|
||||
drop_pending_updates, timeout, secret_token)
|
||||
|
||||
|
||||
|
||||
@@ -1500,11 +1509,11 @@ class AsyncTeleBot:
|
||||
result = await asyncio_helper.get_chat_administrators(self.token, chat_id)
|
||||
return [types.ChatMember.de_json(r) for r in result]
|
||||
|
||||
@util.deprecated(deprecation_text="Use get_chat_member_count instead")
|
||||
async def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
|
||||
"""
|
||||
This function is deprecated. Use `get_chat_member_count` instead
|
||||
"""
|
||||
logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.')
|
||||
result = await asyncio_helper.get_chat_member_count(self.token, chat_id)
|
||||
return result
|
||||
|
||||
@@ -1512,7 +1521,7 @@ class AsyncTeleBot:
|
||||
"""
|
||||
Use this method to get the number of members in a chat. Returns Int on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#getchatmemberscount
|
||||
Telegram documentation: https://core.telegram.org/bots/api#getchatmembercount
|
||||
|
||||
:param chat_id:
|
||||
:return:
|
||||
@@ -1550,6 +1559,22 @@ class AsyncTeleBot:
|
||||
result = await asyncio_helper.delete_chat_sticker_set(self.token, chat_id)
|
||||
return result
|
||||
|
||||
async def answer_web_app_query(self, web_app_query_id: str, result: types.InlineQueryResultBase) -> types.SentWebAppMessage:
|
||||
"""
|
||||
Use this method to set the result of an interaction with a Web App and
|
||||
send a corresponding message on behalf of the user to the chat from which
|
||||
the query originated.
|
||||
On success, a SentWebAppMessage object is returned.
|
||||
|
||||
Telegram Documentation: https://core.telegram.org/bots/api#answerwebappquery
|
||||
|
||||
:param web_app_query_id: Unique identifier for the query to be answered
|
||||
:param result: A JSON-serialized object describing the message to be sent
|
||||
:return:
|
||||
"""
|
||||
|
||||
return await asyncio_helper.answer_web_app_query(self.token, web_app_query_id, result)
|
||||
|
||||
async def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> types.ChatMember:
|
||||
"""
|
||||
Use this method to get information about a member of a chat. Returns a ChatMember object on success.
|
||||
@@ -1563,8 +1588,6 @@ class AsyncTeleBot:
|
||||
result = await asyncio_helper.get_chat_member(self.token, chat_id, user_id)
|
||||
return types.ChatMember.de_json(result)
|
||||
|
||||
|
||||
|
||||
async def send_message(
|
||||
self, chat_id: Union[int, str], text: str,
|
||||
parse_mode: Optional[str]=None,
|
||||
@@ -1639,7 +1662,7 @@ class AsyncTeleBot:
|
||||
reply_to_message_id: Optional[int]=None,
|
||||
allow_sending_without_reply: Optional[bool]=None,
|
||||
reply_markup: Optional[REPLY_MARKUP_TYPES]=None,
|
||||
timeout: Optional[int]=None) -> int:
|
||||
timeout: Optional[int]=None) -> types.MessageID:
|
||||
"""
|
||||
Use this method to copy messages of any kind.
|
||||
|
||||
@@ -1659,6 +1682,8 @@ class AsyncTeleBot:
|
||||
:param protect_content:
|
||||
:return: API reply.
|
||||
"""
|
||||
parse_mode = self.parse_mode if (parse_mode is None) else parse_mode
|
||||
|
||||
return types.MessageID.de_json(
|
||||
await asyncio_helper.copy_message(self.token, chat_id, from_chat_id, message_id, caption, parse_mode, caption_entities,
|
||||
disable_notification, reply_to_message_id, allow_sending_without_reply, reply_markup,
|
||||
@@ -1903,7 +1928,9 @@ class AsyncTeleBot:
|
||||
"""
|
||||
if data and not(sticker):
|
||||
# function typo miss compatibility
|
||||
logger.warning("send_sticker: data parameter is deprecated. Use sticker instead.")
|
||||
sticker = data
|
||||
|
||||
return types.Message.de_json(
|
||||
await asyncio_helper.send_data(
|
||||
self.token, chat_id, sticker, 'sticker',
|
||||
@@ -1954,6 +1981,7 @@ class AsyncTeleBot:
|
||||
parse_mode = self.parse_mode if (parse_mode is None) else parse_mode
|
||||
if data and not(video):
|
||||
# function typo miss compatibility
|
||||
logger.warning("send_sticker: data parameter is deprecated. Use video instead.")
|
||||
video = data
|
||||
|
||||
return types.Message.de_json(
|
||||
@@ -2374,6 +2402,7 @@ class AsyncTeleBot:
|
||||
can_promote_members: Optional[bool]=None,
|
||||
is_anonymous: Optional[bool]=None,
|
||||
can_manage_chat: Optional[bool]=None,
|
||||
can_manage_video_chats: Optional[bool]=None,
|
||||
can_manage_voice_chats: Optional[bool]=None) -> bool:
|
||||
"""
|
||||
Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator
|
||||
@@ -2400,15 +2429,22 @@ class AsyncTeleBot:
|
||||
message statistics in channels, see channel members,
|
||||
see anonymous administrators in supergroups and ignore slow mode.
|
||||
Implied by any other administrator privilege
|
||||
:param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats
|
||||
:param can_manage_video_chats: Bool: Pass True, if the administrator can manage voice chats
|
||||
For now, bots can use this privilege only for passing to other administrators.
|
||||
:param can_manage_voice_chats: Deprecated, use can_manage_video_chats
|
||||
:return: True on success.
|
||||
"""
|
||||
|
||||
if can_manage_voice_chats is not None:
|
||||
logger.warning("promote_chat_member: can_manage_voice_chats parameter is deprecated. Use can_manage_video_chats instead.")
|
||||
if can_manage_video_chats is None:
|
||||
can_manage_video_chats = can_manage_voice_chats
|
||||
|
||||
return await asyncio_helper.promote_chat_member(
|
||||
self.token, chat_id, user_id, can_change_info, can_post_messages,
|
||||
can_edit_messages, can_delete_messages, can_invite_users,
|
||||
can_restrict_members, can_pin_messages, can_promote_members,
|
||||
is_anonymous, can_manage_chat, can_manage_voice_chats)
|
||||
is_anonymous, can_manage_chat, can_manage_video_chats)
|
||||
|
||||
async def set_chat_administrator_custom_title(
|
||||
self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool:
|
||||
@@ -2635,6 +2671,70 @@ class AsyncTeleBot:
|
||||
result = await asyncio_helper.get_my_commands(self.token, scope, language_code)
|
||||
return [types.BotCommand.de_json(cmd) for cmd in result]
|
||||
|
||||
async def set_chat_menu_button(self, chat_id: Union[int, str]=None,
|
||||
menu_button: types.MenuButton=None) -> bool:
|
||||
"""
|
||||
Use this method to change the bot's menu button in a private chat,
|
||||
or the default menu button.
|
||||
Returns True on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#setchatmenubutton
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat.
|
||||
If not specified, default bot's menu button will be changed.
|
||||
:param menu_button: A JSON-serialized object for the new bot's menu button. Defaults to MenuButtonDefault
|
||||
|
||||
"""
|
||||
return await asyncio_helper.set_chat_menu_button(self.token, chat_id, menu_button)
|
||||
|
||||
|
||||
async def get_chat_menu_button(self, chat_id: Union[int, str]=None) -> types.MenuButton:
|
||||
"""
|
||||
Use this method to get the current value of the bot's menu button
|
||||
in a private chat, or the default menu button.
|
||||
Returns MenuButton on success.
|
||||
|
||||
Telegram Documentation: https://core.telegram.org/bots/api#getchatmenubutton
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat.
|
||||
If not specified, default bot's menu button will be returned.
|
||||
:return: types.MenuButton
|
||||
|
||||
"""
|
||||
return types.MenuButton.de_json(await asyncio_helper.get_chat_menu_button(self.token, chat_id))
|
||||
|
||||
|
||||
async def set_my_default_administrator_rights(self, rights: types.ChatAdministratorRights=None,
|
||||
for_channels: bool=None) -> bool:
|
||||
"""
|
||||
Use this method to change the default administrator rights requested by the bot
|
||||
when it's added as an administrator to groups or channels.
|
||||
These rights will be suggested to users, but they are are free to modify
|
||||
the list before adding the bot.
|
||||
Returns True on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#setmydefaultadministratorrights
|
||||
|
||||
:param rights: A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared.
|
||||
:param for_channels: Pass True to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed.
|
||||
"""
|
||||
|
||||
return await asyncio_helper.set_my_default_administrator_rights(self.token, rights, for_channels)
|
||||
|
||||
|
||||
async def get_my_default_administrator_rights(self, for_channels: bool=None) -> types.ChatAdministratorRights:
|
||||
"""
|
||||
Use this method to get the current default administrator rights of the bot.
|
||||
Returns ChatAdministratorRights on success.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/api#getmydefaultadministratorrights
|
||||
|
||||
:param for_channels: Pass True to get the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be returned.
|
||||
:return: types.ChatAdministratorRights
|
||||
"""
|
||||
|
||||
return types.ChatAdministratorRights.de_json(await asyncio_helper.get_my_default_administrator_rights(self.token, for_channels))
|
||||
|
||||
async def set_my_commands(self, commands: List[types.BotCommand],
|
||||
scope: Optional[types.BotCommandScope]=None,
|
||||
language_code: Optional[str]=None) -> bool:
|
||||
@@ -2974,6 +3074,67 @@ class AsyncTeleBot:
|
||||
max_tip_amount, suggested_tip_amounts, protect_content)
|
||||
return types.Message.de_json(result)
|
||||
|
||||
|
||||
async def create_invoice_link(self,
|
||||
title: str, description: str, payload:str, provider_token: str,
|
||||
currency: str, prices: List[types.LabeledPrice],
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[List[int]]=None,
|
||||
provider_data: Optional[str]=None,
|
||||
photo_url: Optional[str]=None,
|
||||
photo_size: Optional[int]=None,
|
||||
photo_width: Optional[int]=None,
|
||||
photo_height: Optional[int]=None,
|
||||
need_name: Optional[bool]=None,
|
||||
need_phone_number: Optional[bool]=None,
|
||||
need_email: Optional[bool]=None,
|
||||
need_shipping_address: Optional[bool]=None,
|
||||
send_phone_number_to_provider: Optional[bool]=None,
|
||||
send_email_to_provider: Optional[bool]=None,
|
||||
is_flexible: Optional[bool]=None) -> str:
|
||||
|
||||
"""
|
||||
Use this method to create a link for an invoice.
|
||||
Returns the created invoice link as String on success.
|
||||
|
||||
Telegram documentation:
|
||||
https://core.telegram.org/bots/api#createinvoicelink
|
||||
|
||||
:param title: Product name, 1-32 characters
|
||||
:param description: Product description, 1-255 characters
|
||||
:param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user,
|
||||
use for your internal processes.
|
||||
:param provider_token: Payments provider token, obtained via @Botfather
|
||||
:param currency: Three-letter ISO 4217 currency code,
|
||||
see https://core.telegram.org/bots/payments#supported-currencies
|
||||
:param prices: Price breakdown, a list of components
|
||||
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
|
||||
:param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency
|
||||
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest
|
||||
:param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider.
|
||||
A detailed description of required fields should be provided by the payment provider.
|
||||
:param photo_url: URL of the product photo for the invoice. Can be a photo of the goods
|
||||
:param photo_size: Photo size in bytes
|
||||
:param photo_width: Photo width
|
||||
:param photo_height: Photo height
|
||||
:param need_name: Pass True, if you require the user's full name to complete the order
|
||||
:param need_phone_number: Pass True, if you require the user's phone number to complete the order
|
||||
:param need_email: Pass True, if you require the user's email to complete the order
|
||||
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
|
||||
:param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
|
||||
:param send_email_to_provider: Pass True, if user's email address should be sent to provider
|
||||
:param is_flexible: Pass True, if the final price depends on the shipping method
|
||||
|
||||
:return: Created invoice link as String on success.
|
||||
"""
|
||||
result = await asyncio_helper.create_invoice_link(
|
||||
self.token, title, description, payload, provider_token,
|
||||
currency, prices, max_tip_amount, suggested_tip_amounts, provider_data,
|
||||
photo_url, photo_size, photo_width, photo_height, need_name, need_phone_number,
|
||||
need_email, need_shipping_address, send_phone_number_to_provider,
|
||||
send_email_to_provider, is_flexible)
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
async def send_poll(
|
||||
self, chat_id: Union[int, str], question: str, options: List[str],
|
||||
@@ -3022,6 +3183,8 @@ class AsyncTeleBot:
|
||||
if isinstance(question, types.Poll):
|
||||
raise RuntimeError("The send_poll signature was changed, please see send_poll function details.")
|
||||
|
||||
explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode
|
||||
|
||||
return types.Message.de_json(
|
||||
await asyncio_helper.send_poll(
|
||||
self.token, chat_id,
|
||||
|
||||
@@ -10,8 +10,12 @@ class SimpleCustomFilter(ABC):
|
||||
Simple Custom Filter base class.
|
||||
Create child class with check() method.
|
||||
Accepts only message, returns bool value, that is compared with given in handler.
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
async def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
@@ -26,8 +30,12 @@ class AdvancedCustomFilter(ABC):
|
||||
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
|
||||
message: Message class
|
||||
text: Filter value given in handler
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
async def check(self, message, text):
|
||||
"""
|
||||
Perform a check.
|
||||
@@ -39,7 +47,7 @@ class TextFilter:
|
||||
"""
|
||||
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
|
||||
|
||||
example of usage is in examples/custom_filters/advanced_text_filter.py
|
||||
example of usage is in examples/asynchronous_telebot/custom_filters/advanced_text_filter.py
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
|
||||
@@ -29,6 +29,7 @@ class StatesGroup:
|
||||
if not name.startswith('__') and not callable(value) and isinstance(value, State):
|
||||
# change value of that variable
|
||||
value.name = ':'.join((cls.__name__, name))
|
||||
value.group = cls
|
||||
|
||||
|
||||
class SkipHandler:
|
||||
|
||||
@@ -11,7 +11,6 @@ API_URL = 'https://api.telegram.org/bot{0}/{1}'
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import telebot
|
||||
from telebot import util, logger
|
||||
|
||||
|
||||
@@ -20,23 +19,22 @@ session = None
|
||||
|
||||
FILE_URL = None
|
||||
|
||||
CONNECT_TIMEOUT = 15
|
||||
READ_TIMEOUT = 30
|
||||
|
||||
LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates)
|
||||
REQUEST_TIMEOUT = 10
|
||||
REQUEST_TIMEOUT = None
|
||||
MAX_RETRIES = 3
|
||||
logger = telebot.logger
|
||||
|
||||
|
||||
REQUEST_LIMIT = 50
|
||||
|
||||
class SessionManager:
|
||||
def __init__(self) -> None:
|
||||
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT))
|
||||
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(
|
||||
limit=REQUEST_LIMIT
|
||||
))
|
||||
|
||||
|
||||
async def create_session(self):
|
||||
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT))
|
||||
self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(
|
||||
limit=REQUEST_LIMIT
|
||||
))
|
||||
return self.session
|
||||
|
||||
async def get_session(self):
|
||||
@@ -63,18 +61,19 @@ async def _process_request(token, url, method='get', params=None, files=None, re
|
||||
while not got_result and current_try<MAX_RETRIES-1:
|
||||
current_try +=1
|
||||
try:
|
||||
async with session.request(method=method, url=API_URL.format(token, url), data=params, timeout=timeout) as resp:
|
||||
async with session.request(method=method, url=API_URL.format(token, url), data=params, timeout=timeout, proxy=proxy) as resp:
|
||||
got_result = True
|
||||
logger.debug("Request: method={0} url={1} params={2} files={3} request_timeout={4} current_try={5}".format(method, url, params, files, request_timeout, current_try).replace(token, token.split(':')[0] + ":{TOKEN}"))
|
||||
|
||||
json_result = await _check_result(url, resp)
|
||||
if json_result:
|
||||
got_result = True
|
||||
return json_result['result']
|
||||
except (ApiTelegramException,ApiInvalidJSONException, ApiHTTPException) as e:
|
||||
raise e
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error('Aiohttp ClientError: {0}'.format(e.__class__.__name__))
|
||||
except Exception as e:
|
||||
logger.error(f'Unkown error: {e.__class__.__name__}')
|
||||
logger.error(f'Unknown error: {e.__class__.__name__}')
|
||||
if not got_result:
|
||||
raise RequestTimeout("Request timeout. Request: method={0} url={1} params={2} files={3} request_timeout={4}".format(method, url, params, files, request_timeout, current_try))
|
||||
|
||||
@@ -172,7 +171,7 @@ async def download_file(token, file_path):
|
||||
|
||||
|
||||
async def set_webhook(token, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None,
|
||||
drop_pending_updates = None, timeout=None):
|
||||
drop_pending_updates = None, timeout=None, secret_token=None):
|
||||
method_url = r'setWebhook'
|
||||
payload = {
|
||||
'url': url if url else "",
|
||||
@@ -190,6 +189,8 @@ async def set_webhook(token, url=None, certificate=None, max_connections=None, a
|
||||
payload['drop_pending_updates'] = drop_pending_updates
|
||||
if timeout:
|
||||
payload['timeout'] = timeout
|
||||
if secret_token:
|
||||
payload['secret_token'] = secret_token
|
||||
return await _process_request(token, method_url, params=payload, files=files)
|
||||
|
||||
|
||||
@@ -218,11 +219,11 @@ async def get_updates(token, offset=None, limit=None,
|
||||
params = {}
|
||||
if offset:
|
||||
params['offset'] = offset
|
||||
elif limit:
|
||||
if limit:
|
||||
params['limit'] = limit
|
||||
elif timeout:
|
||||
if timeout:
|
||||
params['timeout'] = timeout
|
||||
elif allowed_updates:
|
||||
if allowed_updates:
|
||||
params['allowed_updates'] = allowed_updates
|
||||
return await _process_request(token, method_name, params=params, request_timeout=request_timeout)
|
||||
|
||||
@@ -357,6 +358,12 @@ async def delete_chat_sticker_set(token, chat_id):
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
|
||||
async def answer_web_app_query(token, web_app_query_id, result: types.InlineQueryResultBase):
|
||||
method_url = 'answerWebAppQuery'
|
||||
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
async def get_chat_member(token, chat_id, user_id):
|
||||
method_url = r'getChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
@@ -941,7 +948,7 @@ async def promote_chat_member(
|
||||
token, chat_id, user_id, can_change_info=None, can_post_messages=None,
|
||||
can_edit_messages=None, can_delete_messages=None, can_invite_users=None,
|
||||
can_restrict_members=None, can_pin_messages=None, can_promote_members=None,
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_voice_chats=None):
|
||||
is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None):
|
||||
method_url = 'promoteChatMember'
|
||||
payload = {'chat_id': chat_id, 'user_id': user_id}
|
||||
if can_change_info is not None:
|
||||
@@ -964,8 +971,8 @@ async def promote_chat_member(
|
||||
payload['is_anonymous'] = is_anonymous
|
||||
if can_manage_chat is not None:
|
||||
payload['can_manage_chat'] = can_manage_chat
|
||||
if can_manage_voice_chats is not None:
|
||||
payload['can_manage_voice_chats'] = can_manage_voice_chats
|
||||
if can_manage_video_chats is not None:
|
||||
payload['can_manage_video_chats'] = can_manage_video_chats
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
@@ -1106,6 +1113,42 @@ async def get_my_commands(token, scope=None, language_code=None):
|
||||
payload['language_code'] = language_code
|
||||
return await _process_request(token, method_url, params=payload)
|
||||
|
||||
async def set_chat_menu_button(token, chat_id=None, menu_button=None):
|
||||
method_url = r'setChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
if menu_button:
|
||||
payload['menu_button'] = menu_button.to_json()
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def get_chat_menu_button(token, chat_id=None):
|
||||
method_url = r'getChatMenuButton'
|
||||
payload = {}
|
||||
if chat_id:
|
||||
payload['chat_id'] = chat_id
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
async def set_my_default_administrator_rights(token, rights=None, for_channels=None):
|
||||
method_url = r'setMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if rights:
|
||||
payload['rights'] = rights.to_json()
|
||||
if for_channels is not None:
|
||||
payload['for_channels'] = for_channels
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def get_my_default_administrator_rights(token, for_channels=None):
|
||||
method_url = r'getMyDefaultAdministratorRights'
|
||||
payload = {}
|
||||
if for_channels:
|
||||
payload['for_channels'] = for_channels
|
||||
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
async def set_my_commands(token, commands, scope=None, language_code=None):
|
||||
method_url = r'setMyCommands'
|
||||
@@ -1501,7 +1544,6 @@ async def create_new_sticker_set(
|
||||
contains_masks=None, mask_position=None, webm_sticker=None):
|
||||
method_url = 'createNewStickerSet'
|
||||
payload = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
|
||||
stype = None
|
||||
if png_sticker:
|
||||
stype = 'png_sticker'
|
||||
elif webm_sticker:
|
||||
@@ -1526,7 +1568,6 @@ async def create_new_sticker_set(
|
||||
async def add_sticker_to_set(token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker):
|
||||
method_url = 'addStickerToSet'
|
||||
payload = {'user_id': user_id, 'name': name, 'emojis': emojis}
|
||||
stype = None
|
||||
if png_sticker:
|
||||
stype = 'png_sticker'
|
||||
elif webm_sticker:
|
||||
@@ -1560,6 +1601,47 @@ async def delete_sticker_from_set(token, sticker):
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
|
||||
async def create_invoice_link(token, title, description, payload, provider_token,
|
||||
currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None,
|
||||
photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None,
|
||||
need_email=None, need_shipping_address=None, send_phone_number_to_provider=None,
|
||||
send_email_to_provider=None, is_flexible=None):
|
||||
method_url = r'createInvoiceLink'
|
||||
payload = {'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token,
|
||||
'currency': currency, 'prices': await _convert_list_json_serializable(prices)}
|
||||
if max_tip_amount:
|
||||
payload['max_tip_amount'] = max_tip_amount
|
||||
if suggested_tip_amounts:
|
||||
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
|
||||
if provider_data:
|
||||
payload['provider_data'] = provider_data
|
||||
if photo_url:
|
||||
payload['photo_url'] = photo_url
|
||||
if photo_size:
|
||||
payload['photo_size'] = photo_size
|
||||
if photo_width:
|
||||
payload['photo_width'] = photo_width
|
||||
if photo_height:
|
||||
payload['photo_height'] = photo_height
|
||||
if need_name is not None:
|
||||
payload['need_name'] = need_name
|
||||
if need_phone_number is not None:
|
||||
payload['need_phone_number'] = need_phone_number
|
||||
if need_email is not None:
|
||||
payload['need_email'] = need_email
|
||||
if need_shipping_address is not None:
|
||||
payload['need_shipping_address'] = need_shipping_address
|
||||
if send_phone_number_to_provider is not None:
|
||||
payload['send_phone_number_to_provider'] = send_phone_number_to_provider
|
||||
if send_email_to_provider is not None:
|
||||
payload['send_email_to_provider'] = send_email_to_provider
|
||||
if is_flexible is not None:
|
||||
payload['is_flexible'] = is_flexible
|
||||
return await _process_request(token, method_url, params=payload, method='post')
|
||||
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
async def send_poll(
|
||||
token, chat_id,
|
||||
|
||||
@@ -8,7 +8,7 @@ class StateMemoryStorage(StateStorageBase):
|
||||
|
||||
|
||||
async def set_state(self, chat_id, user_id, state):
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
|
||||
import os
|
||||
|
||||
|
||||
import pickle
|
||||
|
||||
|
||||
@@ -47,14 +46,16 @@ class StatePickleStorage(StateStorageBase):
|
||||
file.close()
|
||||
|
||||
async def set_state(self, chat_id, user_id, state):
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
self.data[chat_id][user_id]['state'] = state
|
||||
self.update_data()
|
||||
return True
|
||||
else:
|
||||
self.data[chat_id][user_id] = {'state': state, 'data': {}}
|
||||
self.update_data()
|
||||
return True
|
||||
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
|
||||
self.update_data()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
|
||||
import json
|
||||
|
||||
|
||||
redis_installed = True
|
||||
try:
|
||||
import aioredis
|
||||
@@ -34,7 +35,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to get record from database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
result = await self.redis.get(self.prefix+str(key))
|
||||
if result: return json.loads(result)
|
||||
@@ -44,7 +45,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to set record to database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
|
||||
await self.redis.set(self.prefix+str(key), json.dumps(value))
|
||||
@@ -54,7 +55,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to delete record from database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
await self.redis.delete(self.prefix+str(key))
|
||||
return True
|
||||
@@ -65,7 +66,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
response = await self.get_record(chat_id)
|
||||
user_id = str(user_id)
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if response:
|
||||
if user_id in response:
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2017-2018 Alex Root Junior
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies
|
||||
or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
This file was added during the pull request. The maintainers overlooked that it was copied
|
||||
"as is" from another project and they do not consider it as a right way to develop a project.
|
||||
However, due to backward compatibility we had to leave this file in the project with the above
|
||||
copyright added, as it is required by the original project license.
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ class SimpleCustomFilter(ABC):
|
||||
Simple Custom Filter base class.
|
||||
Create child class with check() method.
|
||||
Accepts only message, returns bool value, that is compared with given in handler.
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
@@ -28,8 +32,12 @@ class AdvancedCustomFilter(ABC):
|
||||
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
|
||||
message: Message class
|
||||
text: Filter value given in handler
|
||||
|
||||
Child classes should have .key property.
|
||||
"""
|
||||
|
||||
key: str = None
|
||||
|
||||
def check(self, message, text):
|
||||
"""
|
||||
Perform a check.
|
||||
|
||||
211
telebot/formatting.py
Normal file
211
telebot/formatting.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Markdown & HTML formatting functions.
|
||||
|
||||
.. versionadded:: 4.5.1
|
||||
"""
|
||||
|
||||
import html
|
||||
import re
|
||||
|
||||
|
||||
def format_text(*args, separator="\n"):
|
||||
"""
|
||||
Formats a list of strings into a single string.
|
||||
|
||||
.. code:: python
|
||||
|
||||
format_text( # just an example
|
||||
mbold('Hello'),
|
||||
mitalic('World')
|
||||
)
|
||||
|
||||
:param args: Strings to format.
|
||||
:param separator: The separator to use between each string.
|
||||
"""
|
||||
return separator.join(args)
|
||||
|
||||
|
||||
|
||||
def escape_html(content: str) -> str:
|
||||
"""
|
||||
Escapes HTML characters in a string of HTML.
|
||||
|
||||
:param content: The string of HTML to escape.
|
||||
"""
|
||||
return html.escape(content)
|
||||
|
||||
|
||||
def escape_markdown(content: str) -> str:
|
||||
"""
|
||||
Escapes Markdown characters in a string of Markdown.
|
||||
|
||||
Credits: simonsmh
|
||||
|
||||
:param content: The string of Markdown to escape.
|
||||
"""
|
||||
|
||||
parse = re.sub(r"([_*\[\]()~`>\#\+\-=|\.!])", r"\\\1", content)
|
||||
reparse = re.sub(r"\\\\([_*\[\]()~`>\#\+\-=|\.!])", r"\1", parse)
|
||||
return reparse
|
||||
|
||||
|
||||
def mbold(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted bold string.
|
||||
|
||||
:param content: The string to bold.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '*{}*'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hbold(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted bold string.
|
||||
|
||||
:param content: The string to bold.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<b>{}</b>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mitalic(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted italic string.
|
||||
|
||||
:param content: The string to italicize.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '_{}_\r'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hitalic(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted italic string.
|
||||
|
||||
:param content: The string to italicize.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<i>{}</i>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def munderline(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted underline string.
|
||||
|
||||
:param content: The string to underline.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '__{}__'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hunderline(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted underline string.
|
||||
|
||||
:param content: The string to underline.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<u>{}</u>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mstrikethrough(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted strikethrough string.
|
||||
|
||||
:param content: The string to strikethrough.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '~{}~'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hstrikethrough(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted strikethrough string.
|
||||
|
||||
:param content: The string to strikethrough.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<s>{}</s>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mspoiler(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted spoiler string.
|
||||
|
||||
:param content: The string to spoiler.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '||{}||'.format(escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hspoiler(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted spoiler string.
|
||||
|
||||
:param content: The string to spoiler.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<tg-spoiler>{}</tg-spoiler>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mlink(content: str, url: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted link string.
|
||||
|
||||
:param content: The string to link.
|
||||
:param url: The URL to link to.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '[{}]({})'.format(escape_markdown(content), escape_markdown(url) if escape else content)
|
||||
|
||||
|
||||
def hlink(content: str, url: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted link string.
|
||||
|
||||
:param content: The string to link.
|
||||
:param url: The URL to link to.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<a href="{}">{}</a>'.format(escape_html(url), escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def mcode(content: str, language: str="", escape: bool=True) -> str:
|
||||
"""
|
||||
Returns a Markdown-formatted code string.
|
||||
|
||||
:param content: The string to code.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '```{}\n{}```'.format(language, escape_markdown(content) if escape else content)
|
||||
|
||||
|
||||
def hcode(content: str, escape: bool=True) -> str:
|
||||
"""
|
||||
Returns an HTML-formatted code string.
|
||||
|
||||
:param content: The string to code.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<code>{}</code>'.format(escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def hpre(content: str, escape: bool=True, language: str="") -> str:
|
||||
"""
|
||||
Returns an HTML-formatted preformatted string.
|
||||
|
||||
:param content: The string to preformatted.
|
||||
:param escape: True if you need to escape special characters.
|
||||
"""
|
||||
return '<pre><code class="{}">{}</code></pre>'.format(language, escape_html(content) if escape else content)
|
||||
|
||||
|
||||
def hide_link(url: str) -> str:
|
||||
"""
|
||||
Hide url of an image.
|
||||
|
||||
:param url:
|
||||
:return: str
|
||||
"""
|
||||
return f'<a href="{url}">⁠</a>'
|
||||
@@ -163,6 +163,10 @@ class StatesGroup:
|
||||
if not name.startswith('__') and not callable(value) and isinstance(value, State):
|
||||
# change value of that variable
|
||||
value.name = ':'.join((cls.__name__, name))
|
||||
value.group = cls
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseMiddleware:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from telebot.storage.base_storage import StateStorageBase, StateContext
|
||||
|
||||
|
||||
class StateMemoryStorage(StateStorageBase):
|
||||
def __init__(self) -> None:
|
||||
self.data = {}
|
||||
@@ -8,7 +9,7 @@ class StateMemoryStorage(StateStorageBase):
|
||||
|
||||
|
||||
def set_state(self, chat_id, user_id, state):
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from telebot.storage.base_storage import StateStorageBase, StateContext
|
||||
import os
|
||||
|
||||
|
||||
import pickle
|
||||
|
||||
|
||||
@@ -53,14 +52,16 @@ class StatePickleStorage(StateStorageBase):
|
||||
file.close()
|
||||
|
||||
def set_state(self, chat_id, user_id, state):
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
if chat_id in self.data:
|
||||
if user_id in self.data[chat_id]:
|
||||
self.data[chat_id][user_id]['state'] = state
|
||||
self.update_data()
|
||||
return True
|
||||
else:
|
||||
self.data[chat_id][user_id] = {'state': state, 'data': {}}
|
||||
self.update_data()
|
||||
return True
|
||||
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
|
||||
self.update_data()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from pyclbr import Class
|
||||
from telebot.storage.base_storage import StateStorageBase, StateContext
|
||||
import json
|
||||
|
||||
@@ -29,7 +28,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to get record from database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
connection = Redis(connection_pool=self.redis)
|
||||
result = connection.get(self.prefix+str(key))
|
||||
@@ -41,7 +40,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to set record to database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
connection = Redis(connection_pool=self.redis)
|
||||
connection.set(self.prefix+str(key), json.dumps(value))
|
||||
@@ -52,7 +51,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
Function to delete record from database.
|
||||
It has nothing to do with states.
|
||||
Made for backend compatibility
|
||||
Made for backward compatibility
|
||||
"""
|
||||
connection = Redis(connection_pool=self.redis)
|
||||
connection.delete(self.prefix+str(key))
|
||||
@@ -65,7 +64,7 @@ class StateRedisStorage(StateStorageBase):
|
||||
"""
|
||||
response = self.get_record(chat_id)
|
||||
user_id = str(user_id)
|
||||
if isinstance(state, object):
|
||||
if hasattr(state, 'name'):
|
||||
state = state.name
|
||||
|
||||
if response:
|
||||
@@ -177,4 +176,4 @@ class StateRedisStorage(StateStorageBase):
|
||||
response[user_id]['data'] = dict(data, **response[user_id]['data'])
|
||||
self.set_record(chat_id, response)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
276
telebot/types.py
276
telebot/types.py
@@ -194,14 +194,15 @@ class WebhookInfo(JsonDeserializable):
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, url, has_custom_certificate, pending_update_count, ip_address=None,
|
||||
last_error_date=None, last_error_message=None, max_connections=None,
|
||||
allowed_updates=None, **kwargs):
|
||||
last_error_date=None, last_error_message=None, last_synchronization_error_date=None,
|
||||
max_connections=None, allowed_updates=None, **kwargs):
|
||||
self.url = url
|
||||
self.has_custom_certificate = has_custom_certificate
|
||||
self.pending_update_count = pending_update_count
|
||||
self.ip_address = ip_address
|
||||
self.last_error_date = last_error_date
|
||||
self.last_error_message = last_error_message
|
||||
self.last_synchronization_error_date = last_synchronization_error_date
|
||||
self.max_connections = max_connections
|
||||
self.allowed_updates = allowed_updates
|
||||
|
||||
@@ -214,7 +215,8 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable):
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, id, is_bot, first_name, last_name=None, username=None, language_code=None,
|
||||
can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None, **kwargs):
|
||||
can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None,
|
||||
is_premium=None, added_to_attachment_menu=None, **kwargs):
|
||||
self.id: int = id
|
||||
self.is_bot: bool = is_bot
|
||||
self.first_name: str = first_name
|
||||
@@ -224,6 +226,9 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable):
|
||||
self.can_join_groups: bool = can_join_groups
|
||||
self.can_read_all_group_messages: bool = can_read_all_group_messages
|
||||
self.supports_inline_queries: bool = supports_inline_queries
|
||||
self.is_premium: bool = is_premium
|
||||
self.added_to_attachment_menu: bool = added_to_attachment_menu
|
||||
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
@@ -279,7 +284,8 @@ class Chat(JsonDeserializable):
|
||||
description=None, invite_link=None, pinned_message=None,
|
||||
permissions=None, slow_mode_delay=None,
|
||||
message_auto_delete_time=None, has_protected_content=None, sticker_set_name=None,
|
||||
can_set_sticker_set=None, linked_chat_id=None, location=None, **kwargs):
|
||||
can_set_sticker_set=None, linked_chat_id=None, location=None,
|
||||
join_to_send_messages=None, join_by_request=None, **kwargs):
|
||||
self.id: int = id
|
||||
self.type: str = type
|
||||
self.title: str = title
|
||||
@@ -288,6 +294,8 @@ class Chat(JsonDeserializable):
|
||||
self.last_name: str = last_name
|
||||
self.photo: ChatPhoto = photo
|
||||
self.bio: str = bio
|
||||
self.join_to_send_messages: bool = join_to_send_messages
|
||||
self.join_by_request: bool = join_by_request
|
||||
self.has_private_forwards: bool = has_private_forwards
|
||||
self.description: str = description
|
||||
self.invite_link: str = invite_link
|
||||
@@ -313,6 +321,20 @@ class MessageID(JsonDeserializable):
|
||||
self.message_id = message_id
|
||||
|
||||
|
||||
class WebAppData(JsonDeserializable):
|
||||
def __init__(self, data, button_text):
|
||||
self.data = data
|
||||
self.button_text = button_text
|
||||
def to_dict(self):
|
||||
return {'data': self.data, 'button_text': self.button_text}
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
obj = cls.check_json(json_string)
|
||||
return cls(**obj)
|
||||
|
||||
|
||||
class Message(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
@@ -457,18 +479,25 @@ class Message(JsonDeserializable):
|
||||
opts['proximity_alert_triggered'] = ProximityAlertTriggered.de_json(obj[
|
||||
'proximity_alert_triggered'])
|
||||
content_type = 'proximity_alert_triggered'
|
||||
if 'voice_chat_scheduled' in obj:
|
||||
opts['voice_chat_scheduled'] = VoiceChatScheduled.de_json(obj['voice_chat_scheduled'])
|
||||
content_type = 'voice_chat_scheduled'
|
||||
if 'voice_chat_started' in obj:
|
||||
opts['voice_chat_started'] = VoiceChatStarted.de_json(obj['voice_chat_started'])
|
||||
content_type = 'voice_chat_started'
|
||||
if 'voice_chat_ended' in obj:
|
||||
opts['voice_chat_ended'] = VoiceChatEnded.de_json(obj['voice_chat_ended'])
|
||||
content_type = 'voice_chat_ended'
|
||||
if 'voice_chat_participants_invited' in obj:
|
||||
opts['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json(obj['voice_chat_participants_invited'])
|
||||
content_type = 'voice_chat_participants_invited'
|
||||
if 'video_chat_scheduled' in obj:
|
||||
opts['video_chat_scheduled'] = VideoChatScheduled.de_json(obj['video_chat_scheduled'])
|
||||
opts['voice_chat_scheduled'] = opts['video_chat_scheduled'] # deprecated, for backward compatibility
|
||||
content_type = 'video_chat_scheduled'
|
||||
if 'video_chat_started' in obj:
|
||||
opts['video_chat_started'] = VideoChatStarted.de_json(obj['video_chat_started'])
|
||||
opts['voice_chat_started'] = opts['video_chat_started'] # deprecated, for backward compatibility
|
||||
content_type = 'video_chat_started'
|
||||
if 'video_chat_ended' in obj:
|
||||
opts['video_chat_ended'] = VideoChatEnded.de_json(obj['video_chat_ended'])
|
||||
opts['voice_chat_ended'] = opts['video_chat_ended'] # deprecated, for backward compatibility
|
||||
content_type = 'video_chat_ended'
|
||||
if 'video_chat_participants_invited' in obj:
|
||||
opts['video_chat_participants_invited'] = VideoChatParticipantsInvited.de_json(obj['video_chat_participants_invited'])
|
||||
opts['voice_chat_participants_invited'] = opts['video_chat_participants_invited'] # deprecated, for backward compatibility
|
||||
content_type = 'video_chat_participants_invited'
|
||||
if 'web_app_data' in obj:
|
||||
opts['web_app_data'] = WebAppData.de_json(obj['web_app_data'])
|
||||
content_type = 'web_app_data'
|
||||
if 'message_auto_delete_timer_changed' in obj:
|
||||
opts['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json(obj['message_auto_delete_timer_changed'])
|
||||
content_type = 'message_auto_delete_timer_changed'
|
||||
@@ -887,7 +916,7 @@ class File(JsonDeserializable):
|
||||
obj = cls.check_json(json_string, dict_copy=False)
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, file_id, file_unique_id, file_size, file_path, **kwargs):
|
||||
def __init__(self, file_id, file_unique_id, file_size=None, file_path=None, **kwargs):
|
||||
self.file_id: str = file_id
|
||||
self.file_unique_id: str = file_unique_id
|
||||
self.file_size: int = file_size
|
||||
@@ -919,6 +948,20 @@ class ReplyKeyboardRemove(JsonSerializable):
|
||||
return json.dumps(json_dict)
|
||||
|
||||
|
||||
class WebAppInfo(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
obj = cls.check_json(json_string)
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, url, **kwargs):
|
||||
self.url: str = url
|
||||
|
||||
def to_dict(self):
|
||||
return {'url': self.url}
|
||||
|
||||
|
||||
class ReplyKeyboardMarkup(JsonSerializable):
|
||||
max_row_keys = 12
|
||||
|
||||
@@ -1011,11 +1054,13 @@ class KeyboardButtonPollType(Dictionaryable):
|
||||
|
||||
class KeyboardButton(Dictionaryable, JsonSerializable):
|
||||
def __init__(self, text: str, request_contact: Optional[bool]=None,
|
||||
request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None):
|
||||
request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None,
|
||||
web_app: WebAppInfo=None):
|
||||
self.text: str = text
|
||||
self.request_contact: bool = request_contact
|
||||
self.request_location: bool = request_location
|
||||
self.request_poll: KeyboardButtonPollType = request_poll
|
||||
self.web_app: WebAppInfo = web_app
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict())
|
||||
@@ -1028,6 +1073,8 @@ class KeyboardButton(Dictionaryable, JsonSerializable):
|
||||
json_dict['request_location'] = self.request_location
|
||||
if self.request_poll is not None:
|
||||
json_dict['request_poll'] = self.request_poll.to_dict()
|
||||
if self.web_app is not None:
|
||||
json_dict['web_app'] = self.web_app.to_dict()
|
||||
return json_dict
|
||||
|
||||
|
||||
@@ -1122,13 +1169,17 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
|
||||
obj = cls.check_json(json_string)
|
||||
if 'login_url' in obj:
|
||||
obj['login_url'] = LoginUrl.de_json(obj.get('login_url'))
|
||||
if 'web_app' in obj:
|
||||
obj['web_app'] = WebAppInfo.de_json(obj.get('web_app'))
|
||||
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, text, url=None, callback_data=None, switch_inline_query=None,
|
||||
def __init__(self, text, url=None, callback_data=None, web_app=None, switch_inline_query=None,
|
||||
switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **kwargs):
|
||||
self.text: str = text
|
||||
self.url: str = url
|
||||
self.callback_data: str = callback_data
|
||||
self.web_app: WebAppInfo = web_app
|
||||
self.switch_inline_query: str = switch_inline_query
|
||||
self.switch_inline_query_current_chat: str = switch_inline_query_current_chat
|
||||
self.callback_game = callback_game # Not Implemented
|
||||
@@ -1144,6 +1195,8 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable)
|
||||
json_dict['url'] = self.url
|
||||
if self.callback_data:
|
||||
json_dict['callback_data'] = self.callback_data
|
||||
if self.web_app:
|
||||
json_dict['web_app'] = self.web_app.to_dict()
|
||||
if self.switch_inline_query is not None:
|
||||
json_dict['switch_inline_query'] = self.switch_inline_query
|
||||
if self.switch_inline_query_current_chat is not None:
|
||||
@@ -1235,7 +1288,7 @@ class ChatMember(JsonDeserializable):
|
||||
can_invite_users=None, can_pin_messages=None, is_member=None,
|
||||
can_send_messages=None, can_send_media_messages=None, can_send_polls=None,
|
||||
can_send_other_messages=None, can_add_web_page_previews=None,
|
||||
can_manage_chat=None, can_manage_voice_chats=None,
|
||||
can_manage_chat=None, can_manage_video_chats=None,
|
||||
until_date=None, **kwargs):
|
||||
self.user: User = user
|
||||
self.status: str = status
|
||||
@@ -1257,7 +1310,8 @@ class ChatMember(JsonDeserializable):
|
||||
self.can_send_other_messages: bool = can_send_other_messages
|
||||
self.can_add_web_page_previews: bool = can_add_web_page_previews
|
||||
self.can_manage_chat: bool = can_manage_chat
|
||||
self.can_manage_voice_chats: bool = can_manage_voice_chats
|
||||
self.can_manage_video_chats: bool = can_manage_video_chats
|
||||
self.can_manage_voice_chats: bool = self.can_manage_video_chats # deprecated, for backward compatibility
|
||||
self.until_date: int = until_date
|
||||
|
||||
|
||||
@@ -1689,6 +1743,26 @@ class InlineQueryResultBase(ABC, Dictionaryable, JsonSerializable):
|
||||
return json_dict
|
||||
|
||||
|
||||
class SentWebAppMessage(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
obj = cls.check_json(json_string)
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, inline_message_id=None):
|
||||
self.inline_message_id = inline_message_id
|
||||
|
||||
def to_dict(self):
|
||||
json_dict = {}
|
||||
if self.inline_message_id:
|
||||
json_dict['inline_message_id'] = self.inline_message_id
|
||||
return json_dict
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InlineQueryResultArticle(InlineQueryResultBase):
|
||||
def __init__(self, id, title, input_message_content, reply_markup=None,
|
||||
url=None, hide_url=None, description=None, thumb_url=None, thumb_width=None, thumb_height=None):
|
||||
@@ -2509,10 +2583,13 @@ class Sticker(JsonDeserializable):
|
||||
obj['thumb'] = None
|
||||
if 'mask_position' in obj:
|
||||
obj['mask_position'] = MaskPosition.de_json(obj['mask_position'])
|
||||
if 'premium_animation' in obj:
|
||||
obj['premium_animation'] = File.de_json(obj['premium_animation'])
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, file_id, file_unique_id, width, height, is_animated,
|
||||
is_video, thumb=None, emoji=None, set_name=None, mask_position=None, file_size=None, **kwargs):
|
||||
is_video, thumb=None, emoji=None, set_name=None, mask_position=None, file_size=None,
|
||||
premium_animation=None, **kwargs):
|
||||
self.file_id: str = file_id
|
||||
self.file_unique_id: str = file_unique_id
|
||||
self.width: int = width
|
||||
@@ -2524,6 +2601,7 @@ class Sticker(JsonDeserializable):
|
||||
self.set_name: str = set_name
|
||||
self.mask_position: MaskPosition = mask_position
|
||||
self.file_size: int = file_size
|
||||
self.premium_animation: File = premium_animation
|
||||
|
||||
|
||||
|
||||
@@ -2707,23 +2785,29 @@ class Poll(JsonDeserializable):
|
||||
obj['explanation_entities'] = Message.parse_entities(obj['explanation_entities'])
|
||||
return cls(**obj)
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(
|
||||
self,
|
||||
question, options,
|
||||
poll_id=None, total_voter_count=None, is_closed=None, is_anonymous=None, poll_type=None,
|
||||
poll_id=None, total_voter_count=None, is_closed=None, is_anonymous=None, type=None,
|
||||
allows_multiple_answers=None, correct_option_id=None, explanation=None, explanation_entities=None,
|
||||
open_period=None, close_date=None, **kwargs):
|
||||
open_period=None, close_date=None, poll_type=None, **kwargs):
|
||||
self.id: str = poll_id
|
||||
self.question: str = question
|
||||
self.options: List[PollOption] = options
|
||||
self.total_voter_count: int = total_voter_count
|
||||
self.is_closed: bool = is_closed
|
||||
self.is_anonymous: bool = is_anonymous
|
||||
self.type: str = poll_type
|
||||
self.type: str = type
|
||||
if poll_type is not None:
|
||||
# Wrong param name backward compatibility
|
||||
logger.warning("Poll: poll_type parameter is deprecated. Use type instead.")
|
||||
if type is None:
|
||||
self.type: str = poll_type
|
||||
self.allows_multiple_answers: bool = allows_multiple_answers
|
||||
self.correct_option_id: int = correct_option_id
|
||||
self.explanation: str = explanation
|
||||
self.explanation_entities: List[MessageEntity] = explanation_entities # Default state of entities is None. if (explanation_entities is not None) else []
|
||||
self.explanation_entities: List[MessageEntity] = explanation_entities
|
||||
self.open_period: int = open_period
|
||||
self.close_date: int = close_date
|
||||
|
||||
@@ -2833,7 +2917,7 @@ class ProximityAlertTriggered(JsonDeserializable):
|
||||
self.distance: int = distance
|
||||
|
||||
|
||||
class VoiceChatStarted(JsonDeserializable):
|
||||
class VideoChatStarted(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
return cls()
|
||||
@@ -2845,8 +2929,13 @@ class VoiceChatStarted(JsonDeserializable):
|
||||
"""
|
||||
pass
|
||||
|
||||
class VoiceChatStarted(VideoChatStarted):
|
||||
def __init__(self):
|
||||
logger.warning('VoiceChatStarted is deprecated. Use VideoChatStarted instead.')
|
||||
super().__init__()
|
||||
|
||||
class VoiceChatScheduled(JsonDeserializable):
|
||||
|
||||
class VideoChatScheduled(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
@@ -2856,8 +2945,13 @@ class VoiceChatScheduled(JsonDeserializable):
|
||||
def __init__(self, start_date, **kwargs):
|
||||
self.start_date: int = start_date
|
||||
|
||||
class VoiceChatScheduled(VideoChatScheduled):
|
||||
def __init__(self, *args, **kwargs):
|
||||
logger.warning('VoiceChatScheduled is deprecated. Use VideoChatScheduled instead.')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class VoiceChatEnded(JsonDeserializable):
|
||||
|
||||
class VideoChatEnded(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
@@ -2867,8 +2961,14 @@ class VoiceChatEnded(JsonDeserializable):
|
||||
def __init__(self, duration, **kwargs):
|
||||
self.duration: int = duration
|
||||
|
||||
class VoiceChatEnded(VideoChatEnded):
|
||||
def __init__(self, *args, **kwargs):
|
||||
logger.warning('VoiceChatEnded is deprecated. Use VideoChatEnded instead.')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class VoiceChatParticipantsInvited(JsonDeserializable):
|
||||
|
||||
|
||||
class VideoChatParticipantsInvited(JsonDeserializable):
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
@@ -2880,6 +2980,11 @@ class VoiceChatParticipantsInvited(JsonDeserializable):
|
||||
def __init__(self, users=None, **kwargs):
|
||||
self.users: List[User] = users
|
||||
|
||||
class VoiceChatParticipantsInvited(VideoChatParticipantsInvited):
|
||||
def __init__(self, *args, **kwargs):
|
||||
logger.warning('VoiceChatParticipantsInvited is deprecated. Use VideoChatParticipantsInvited instead.')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MessageAutoDeleteTimerChanged(JsonDeserializable):
|
||||
@classmethod
|
||||
@@ -2890,3 +2995,114 @@ class MessageAutoDeleteTimerChanged(JsonDeserializable):
|
||||
|
||||
def __init__(self, message_auto_delete_time, **kwargs):
|
||||
self.message_auto_delete_time = message_auto_delete_time
|
||||
|
||||
|
||||
class MenuButton(JsonDeserializable, JsonSerializable):
|
||||
"""
|
||||
Base class for MenuButtons.
|
||||
"""
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
obj = cls.check_json(json_string)
|
||||
map = {
|
||||
'commands': MenuButtonCommands,
|
||||
'web_app': MenuButtonWebApp,
|
||||
'default': MenuButtonDefault
|
||||
}
|
||||
return map[obj['type']](**obj)
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MenuButtonCommands(MenuButton):
|
||||
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': self.type}
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
class MenuButtonWebApp(MenuButton):
|
||||
|
||||
def __init__(self, type, text, web_app):
|
||||
self.type: str = type
|
||||
self.text: str = text
|
||||
self.web_app: WebAppInfo = web_app
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': self.type, 'text': self.text, 'web_app': self.web_app.to_dict()}
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
class MenuButtonDefault(MenuButton):
|
||||
|
||||
def __init__(self, type):
|
||||
self.type: str = type
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': self.type}
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
|
||||
class ChatAdministratorRights(JsonDeserializable, JsonSerializable):
|
||||
"""
|
||||
Class representation of:
|
||||
https://core.telegram.org/bots/api#chatadministratorrights
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, json_string):
|
||||
if json_string is None: return None
|
||||
obj = cls.check_json(json_string)
|
||||
return cls(**obj)
|
||||
|
||||
def __init__(self, is_anonymous: bool, can_manage_chat: bool,
|
||||
can_delete_messages: bool, can_manage_video_chats: bool, can_restrict_members: bool,
|
||||
can_promote_members: bool, can_change_info: bool, can_invite_users: bool,
|
||||
can_post_messages: bool=None, can_edit_messages: bool=None,
|
||||
can_pin_messages: bool=None) -> None:
|
||||
|
||||
self.is_anonymous: bool = is_anonymous
|
||||
self.can_manage_chat: bool = can_manage_chat
|
||||
self.can_delete_messages: bool = can_delete_messages
|
||||
self.can_manage_video_chats: bool = can_manage_video_chats
|
||||
self.can_restrict_members: bool = can_restrict_members
|
||||
self.can_promote_members: bool = can_promote_members
|
||||
self.can_change_info: bool = can_change_info
|
||||
self.can_invite_users: bool = can_invite_users
|
||||
self.can_post_messages: bool = can_post_messages
|
||||
self.can_edit_messages: bool = can_edit_messages
|
||||
self.can_pin_messages: bool = can_pin_messages
|
||||
|
||||
def to_dict(self):
|
||||
json_dict = {
|
||||
'is_anonymous': self.is_anonymous,
|
||||
'can_manage_chat': self.can_manage_chat,
|
||||
'can_delete_messages': self.can_delete_messages,
|
||||
'can_manage_video_chats': self.can_manage_video_chats,
|
||||
'can_restrict_members': self.can_restrict_members,
|
||||
'can_promote_members': self.can_promote_members,
|
||||
'can_change_info': self.can_change_info,
|
||||
'can_invite_users': self.can_invite_users,
|
||||
}
|
||||
if self.can_post_messages is not None:
|
||||
json_dict['can_post_messages'] = self.can_post_messages
|
||||
if self.can_edit_messages is not None:
|
||||
json_dict['can_edit_messages'] = self.can_edit_messages
|
||||
if self.can_pin_messages is not None:
|
||||
json_dict['can_pin_messages'] = self.can_pin_messages
|
||||
return json_dict
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ import string
|
||||
import threading
|
||||
import traceback
|
||||
from typing import Any, Callable, List, Dict, Optional, Union
|
||||
import hmac
|
||||
import json
|
||||
from hashlib import sha256
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
import queue as Queue
|
||||
@@ -40,8 +44,8 @@ content_type_media = [
|
||||
content_type_service = [
|
||||
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
|
||||
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
||||
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
|
||||
'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
|
||||
'video_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
]
|
||||
|
||||
update_types = [
|
||||
@@ -370,7 +374,8 @@ def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.I
|
||||
'switch_inline_query_current_chat': None,
|
||||
'callback_game': None,
|
||||
'pay': None,
|
||||
'login_url': None
|
||||
'login_url': None,
|
||||
'web_app': None
|
||||
}
|
||||
|
||||
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
|
||||
@@ -449,17 +454,22 @@ def generate_random_token():
|
||||
return ''.join(random.sample(string.ascii_letters, 16))
|
||||
|
||||
|
||||
def deprecated(warn: bool=True, alternative: Optional[Callable]=None):
|
||||
def deprecated(warn: bool=True, alternative: Optional[Callable]=None, deprecation_text=None):
|
||||
"""
|
||||
Use this decorator to mark functions as deprecated.
|
||||
When the function is used, an info (or warning if `warn` is True) is logged.
|
||||
|
||||
:param warn: If True a warning is logged else an info
|
||||
:param alternative: The new function to use instead
|
||||
:param deprecation_text: Custom deprecation text
|
||||
"""
|
||||
def decorator(function):
|
||||
def wrapper(*args, **kwargs):
|
||||
info = f"`{function.__name__}` is deprecated." + (f" Use `{alternative.__name__}` instead" if alternative else "")
|
||||
info = f"`{function.__name__}` is deprecated."
|
||||
if alternative:
|
||||
info += f" Use `{alternative.__name__}` instead"
|
||||
if deprecation_text:
|
||||
info += " " + deprecation_text
|
||||
if not warn:
|
||||
logger.info(info)
|
||||
else:
|
||||
@@ -512,3 +522,36 @@ def antiflood(function, *args, **kwargs):
|
||||
msg = function(*args, **kwargs)
|
||||
finally:
|
||||
return msg
|
||||
|
||||
|
||||
def parse_web_app_data(token: str, raw_init_data: str):
|
||||
is_valid = validate_WebApp_data(token, raw_init_data)
|
||||
if not is_valid:
|
||||
return False
|
||||
|
||||
result = {}
|
||||
for key, value in parse_qsl(raw_init_data):
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
result[key] = value
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def validate_web_app_data(token, raw_init_data):
|
||||
try:
|
||||
parsed_data = dict(parse_qsl(raw_init_data))
|
||||
except ValueError:
|
||||
return False
|
||||
if "hash" not in parsed_data:
|
||||
return False
|
||||
|
||||
init_data_hash = parsed_data.pop('hash')
|
||||
data_check_string = "\n".join(f"{key}={value}" for key, value in sorted(parsed_data.items()))
|
||||
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=sha256)
|
||||
|
||||
return hmac.new(secret_key.digest(), data_check_string.encode(), sha256).hexdigest() == init_data_hash
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '4.4.1'
|
||||
__version__ = '4.6.0'
|
||||
|
||||
@@ -6,10 +6,13 @@ from telebot import types
|
||||
|
||||
|
||||
def test_json_user():
|
||||
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true}'
|
||||
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true, "is_premium":true, "added_to_attachment_menu": true}'
|
||||
u = types.User.de_json(jsonstring)
|
||||
assert u.id == 101176298
|
||||
assert u.full_name == 'RDSSBOT )))'
|
||||
assert u.is_premium is True
|
||||
assert u.added_to_attachment_menu is True
|
||||
|
||||
|
||||
|
||||
def test_json_message():
|
||||
@@ -155,12 +158,14 @@ def test_json_update():
|
||||
|
||||
|
||||
def test_json_chat():
|
||||
json_string = r'{"id": -111111,"title": "Test Title","type": "group"}'
|
||||
json_string = r'{"id": -111111,"title": "Test Title","type": "group", "join_to_send_messages": true, "join_by_request": true}'
|
||||
chat = types.Chat.de_json(json_string)
|
||||
assert chat.id == -111111
|
||||
assert chat.type == 'group'
|
||||
assert chat.title == 'Test Title'
|
||||
|
||||
assert chat.join_to_send_messages is True
|
||||
assert chat.join_by_request is True
|
||||
|
||||
|
||||
def test_InlineQueryResultCachedPhoto():
|
||||
iq = types.InlineQueryResultCachedPhoto('aaa', 'Fileid')
|
||||
@@ -245,3 +250,26 @@ def test_chat_member_updated():
|
||||
assert cm_updated.old_chat_member.status == "member"
|
||||
assert cm_updated.new_chat_member.status == "administrator"
|
||||
|
||||
|
||||
def test_webhook_info():
|
||||
json_string = r'{"url": "https://example.com/webhook", "has_custom_certificate": true, "pending_update_count": 1, "last_error_date": 0, "last_error_message": "", "last_synchronization_error_date": 489309, "max_connections": 40, "allowed_updates": ["message"]}'
|
||||
webhook_info = types.WebhookInfo.de_json(json_string)
|
||||
print(webhook_info)
|
||||
assert webhook_info.url == 'https://example.com/webhook'
|
||||
assert webhook_info.has_custom_certificate is True
|
||||
assert webhook_info.pending_update_count == 1
|
||||
assert webhook_info.last_error_date == 0
|
||||
assert webhook_info.last_error_message == ''
|
||||
assert webhook_info.max_connections == 40
|
||||
assert webhook_info.last_synchronization_error_date == 489309
|
||||
assert webhook_info.allowed_updates == ['message']
|
||||
|
||||
|
||||
def test_sent_web_app_message():
|
||||
json_string = r'{"inline_message_id": "29430"}'
|
||||
sent_web_app_message = types.SentWebAppMessage.de_json(json_string)
|
||||
assert sent_web_app_message.inline_message_id == '29430'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user