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

Compare commits

...

115 Commits

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

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

View File

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

1
.gitignore vendored
View File

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

View File

@@ -11,7 +11,7 @@
<p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p>
<p align="center">Both synchronous and asynchronous.</p>
## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#april-16-2022">6.0</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,6 +727,7 @@ 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)
@@ -816,7 +845,7 @@ Here are some examples of template:
* [TeleBot template](https://github.com/coder2020official/telebot_template)
## Bots using this API
## Bots using this library
* [SiteAlert bot](https://telegram.me/SiteAlert_bot) ([source](https://github.com/ilteoood/SiteAlert-Python)) by *ilteoood* - Monitors websites and sends a notification on changes
* [TelegramLoggingBot](https://github.com/aRandomStranger/TelegramLoggingBot) by *aRandomStranger*
* [Telegram LMGTFY_bot](https://github.com/GabrielRF/telegram-lmgtfy_bot) by *GabrielRF* - Let me Google that for you.
@@ -867,5 +896,8 @@ Here are some examples of template:
* [remoteTelegramShell](https://github.com/EnriqueMoran/remoteTelegramShell) by [EnriqueMoran](https://github.com/EnriqueMoran). Control your LinuxOS computer through Telegram.
* [Pyfram-telegram-bot](https://github.com/skelly37/pyfram-telegram-bot) Query wolframalpha.com and make use of its API through Telegram.
* [TranslateThisVideoBot](https://gitlab.com/WuerfelDev/translatethisvideo) This Bot can understand spoken text in videos and translate it to English
* [Zyprexa](https://t.me/mathemathicsBot) ([source](https://github.com/atif5/zyprexa)) Zyprexa can solve, help you solve any mathematical problem you encounter and convert your regular mathematical expressions into beautiful imagery using LaTeX.
* [Bincode-telegram-bot](https://github.com/tusharhero/bincode-telegram-bot) by [tusharhero](https://github.com/tusharhero) - Makes [bincodes](https://github.com/tusharhero/bincode) from text provides and also converts them back to text.
* [hydrolib_bot](https://github.com/Mayson90/hydrolib_bot) Toolset for Hydrophilia tabletop game (game cards, rules, structure...).
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ copyright = '2022, coder2020official'
author = 'coder2020official'
# The full version, including alpha/beta/rc tags
release = '4.5.0'
release = '4.6.0'
# -- General configuration ---------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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
@@ -261,7 +262,7 @@ class TeleBot:
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,
@@ -286,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):
"""
@@ -384,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:
@@ -441,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)
@@ -470,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)
@@ -542,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.
@@ -566,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))
@@ -580,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.
@@ -596,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.
@@ -615,12 +619,20 @@ class TeleBot:
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
@@ -645,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
@@ -665,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:
@@ -684,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
@@ -704,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:
@@ -727,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':
@@ -1013,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.
@@ -2435,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(
@@ -2483,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,
@@ -3876,7 +3970,7 @@ class TeleBot:
"""
Check middleware
:param message:
:param update_type:
:return:
"""
middlewares = None
@@ -3884,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.
@@ -3905,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:
@@ -3916,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
@@ -3942,6 +4044,7 @@ class TeleBot:
return self.exception_handler.handle(e)
logging.error(str(e))
return
# remove the bot from data
if middlewares:
for middleware in middlewares:
middleware.post_process(message, data, handler_error)

View File

@@ -268,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 "",
@@ -286,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)
@@ -1615,12 +1617,52 @@ def delete_sticker_from_set(token, sticker):
payload = {'sticker': 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'
result = result.to_json()
payload = {'query_id': web_app_query_id, 'result': result}
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,

View File

@@ -277,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)
@@ -295,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
@@ -1384,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,
@@ -1409,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)
@@ -1580,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,
@@ -1656,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.
@@ -1676,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,
@@ -3066,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],
@@ -3114,6 +3183,8 @@ class AsyncTeleBot:
if isinstance(question, types.Poll):
raise RuntimeError("The send_poll signature was changed, please see send_poll function details.")
explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode
return types.Message.de_json(
await asyncio_helper.send_poll(
self.token, chat_id,

View File

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

View File

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

View File

@@ -19,21 +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
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):
@@ -60,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))
@@ -169,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 "",
@@ -187,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)
@@ -215,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)
@@ -353,10 +357,10 @@ async def delete_chat_sticker_set(token, chat_id):
payload = {'chat_id': 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'
result = result.to_json()
payload = {'query_id': web_app_query_id, 'result': result}
payload = {'web_app_query_id': web_app_query_id, 'result': result.to_json()}
return await _process_request(token, method_url, params=payload, method='post')
@@ -1597,6 +1601,47 @@ async def delete_sticker_from_set(token, sticker):
return await _process_request(token, method_url, params=payload, method='post')
async def create_invoice_link(token, title, description, payload, provider_token,
currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None,
photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None,
need_email=None, need_shipping_address=None, send_phone_number_to_provider=None,
send_email_to_provider=None, is_flexible=None):
method_url = r'createInvoiceLink'
payload = {'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token,
'currency': currency, 'prices': await _convert_list_json_serializable(prices)}
if max_tip_amount:
payload['max_tip_amount'] = max_tip_amount
if suggested_tip_amounts:
payload['suggested_tip_amounts'] = json.dumps(suggested_tip_amounts)
if provider_data:
payload['provider_data'] = provider_data
if photo_url:
payload['photo_url'] = photo_url
if photo_size:
payload['photo_size'] = photo_size
if photo_width:
payload['photo_width'] = photo_width
if photo_height:
payload['photo_height'] = photo_height
if need_name is not None:
payload['need_name'] = need_name
if need_phone_number is not None:
payload['need_phone_number'] = need_phone_number
if need_email is not None:
payload['need_email'] = need_email
if need_shipping_address is not None:
payload['need_shipping_address'] = need_shipping_address
if send_phone_number_to_provider is not None:
payload['send_phone_number_to_provider'] = send_phone_number_to_provider
if send_email_to_provider is not None:
payload['send_email_to_provider'] = send_email_to_provider
if is_flexible is not None:
payload['is_flexible'] = is_flexible
return await _process_request(token, method_url, params=payload, method='post')
# noinspection PyShadowingBuiltins
async def send_poll(
token, chat_id,

View File

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

View File

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

View File

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

View File

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

View File

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

211
telebot/formatting.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -215,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
@@ -225,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):
@@ -280,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
@@ -289,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
@@ -909,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
@@ -2576,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
@@ -2591,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
@@ -2774,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
@@ -3076,11 +3093,11 @@ class ChatAdministratorRights(JsonDeserializable, JsonSerializable):
'can_change_info': self.can_change_info,
'can_invite_users': self.can_invite_users,
}
if 'can_post_messages' is not None:
if self.can_post_messages is not None:
json_dict['can_post_messages'] = self.can_post_messages
if 'can_edit_messages' is not None:
if self.can_edit_messages is not None:
json_dict['can_edit_messages'] = self.can_edit_messages
if 'can_pin_messages' is not None:
if self.can_pin_messages is not None:
json_dict['can_pin_messages'] = self.can_pin_messages
return json_dict

View File

@@ -5,6 +5,10 @@ import string
import threading
import traceback
from typing import Any, Callable, List, Dict, Optional, Union
import hmac
import json
from hashlib import sha256
from urllib.parse import parse_qsl
# noinspection PyPep8Naming
import queue as Queue
@@ -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:}
@@ -517,3 +522,36 @@ def antiflood(function, *args, **kwargs):
msg = function(*args, **kwargs)
finally:
return msg
def parse_web_app_data(token: str, raw_init_data: str):
is_valid = validate_WebApp_data(token, raw_init_data)
if not is_valid:
return False
result = {}
for key, value in parse_qsl(raw_init_data):
try:
value = json.loads(value)
except json.JSONDecodeError:
result[key] = value
else:
result[key] = value
return result
def validate_web_app_data(token, raw_init_data):
try:
parsed_data = dict(parse_qsl(raw_init_data))
except ValueError:
return False
if "hash" not in parsed_data:
return False
init_data_hash = parsed_data.pop('hash')
data_check_string = "\n".join(f"{key}={value}" for key, value in sorted(parsed_data.items()))
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=sha256)
return hmac.new(secret_key.digest(), data_check_string.encode(), sha256).hexdigest() == init_data_hash

View File

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

View File

@@ -6,10 +6,13 @@ from telebot import types
def test_json_user():
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true}'
jsonstring = r'{"id":101176298,"first_name":"RDSSBOT","last_name":")))","username":"rdss_bot","is_bot":true, "is_premium":true, "added_to_attachment_menu": true}'
u = types.User.de_json(jsonstring)
assert u.id == 101176298
assert u.full_name == 'RDSSBOT )))'
assert u.is_premium is True
assert u.added_to_attachment_menu is True
def test_json_message():
@@ -155,12 +158,14 @@ def test_json_update():
def test_json_chat():
json_string = r'{"id": -111111,"title": "Test Title","type": "group"}'
json_string = r'{"id": -111111,"title": "Test Title","type": "group", "join_to_send_messages": true, "join_by_request": true}'
chat = types.Chat.de_json(json_string)
assert chat.id == -111111
assert chat.type == 'group'
assert chat.title == 'Test Title'
assert chat.join_to_send_messages is True
assert chat.join_by_request is True
def test_InlineQueryResultCachedPhoto():
iq = types.InlineQueryResultCachedPhoto('aaa', 'Fileid')