mirror of
https://github.com/eternnoir/pyTelegramBotAPI.git
synced 2023-08-10 21:12:57 +03:00
Compare commits
54 Commits
97dbedaa54
...
01a6827542
Author | SHA1 | Date | |
---|---|---|---|
|
01a6827542 | ||
|
b960a9e574 | ||
|
102fe3a8fb | ||
|
292df419ba | ||
|
7993e1d1c9 | ||
|
7309f92c36 | ||
|
7875ff293d | ||
|
4adac4d852 | ||
|
38bff65caf | ||
|
9ecadf1bc1 | ||
|
5d7ae385ec | ||
|
74e9780b30 | ||
|
9b20f41ece | ||
|
967309120e | ||
|
94be2abdbd | ||
|
6c31b53cd9 | ||
|
9bfc0b2c6f | ||
|
fc374ec57a | ||
|
7a8e60ddc2 | ||
|
7f43f26886 | ||
|
4521982837 | ||
|
30c43b557c | ||
|
10b5886dcc | ||
|
93b97fc3fe | ||
|
1f6e60fd74 | ||
|
5337d4838d | ||
|
ae5d183db0 | ||
|
0d85a34551 | ||
|
002c608d45 | ||
|
ec766a3e43 | ||
|
0ef8d04ed2 | ||
|
3a86916e72 | ||
|
b41435f407 | ||
|
f689d90815 | ||
|
966f2e7ef7 | ||
|
9075430210 | ||
|
68095ad69a | ||
|
8c3d1e608c | ||
|
6822f18cbb | ||
|
6e4f2e19d6 | ||
|
8bbd062d13 | ||
|
5f7ccc8c9b | ||
|
5b1483f646 | ||
|
3cd86d0e93 | ||
|
a893fbc358 | ||
|
6fd2a38fe9 | ||
|
b89ecb3e5a | ||
|
2e5590b566 | ||
|
733bb2ebbb | ||
|
64a22457e2 | ||
|
0c8e94d2c6 | ||
|
b9436821e0 | ||
|
a8af9120de | ||
|
0655a1f6b6 |
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -64,3 +64,8 @@ testMain.py
|
||||||
#VS Code
|
#VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# documentation
|
||||||
|
_build/
|
||||||
|
_static/
|
||||||
|
_templates/
|
||||||
|
|
19
.readthedocs.yml
Normal file
19
.readthedocs.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# .readthedocs.yml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/source/conf.py
|
||||||
|
|
||||||
|
# Optionally build your docs in additional formats such as PDF and ePub
|
||||||
|
formats: all
|
||||||
|
|
||||||
|
# Optionally set the version of Python and requirements required to build your docs
|
||||||
|
python:
|
||||||
|
version: 3.7
|
||||||
|
install:
|
||||||
|
- requirements: doc_req.txt
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
## <p align="center">Supporting Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
|
## <p align="center">Supporting Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
|
||||||
|
|
||||||
|
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
* [Getting started](#getting-started)
|
* [Getting started](#getting-started)
|
||||||
|
@ -860,5 +862,6 @@ Here are some examples of template:
|
||||||
* [GrandQuiz Bot](https://github.com/Carlosma7/TFM-GrandQuiz) by [Carlosma7](https://github.com/Carlosma7). This bot is a trivia game that allows you to play with people from different ages. This project addresses the use of a system through chatbots to carry out a social and intergenerational game as an alternative to traditional game development.
|
* [GrandQuiz Bot](https://github.com/Carlosma7/TFM-GrandQuiz) by [Carlosma7](https://github.com/Carlosma7). This bot is a trivia game that allows you to play with people from different ages. This project addresses the use of a system through chatbots to carry out a social and intergenerational game as an alternative to traditional game development.
|
||||||
* [Diccionario de la RAE](https://t.me/dleraebot) ([source](https://github.com/studentenherz/dleraebot)) This bot lets you find difinitions of words in Spanish using [RAE's dictionary](https://dle.rae.es/). It features direct message and inline search.
|
* [Diccionario de la RAE](https://t.me/dleraebot) ([source](https://github.com/studentenherz/dleraebot)) This bot lets you find difinitions of words in Spanish using [RAE's dictionary](https://dle.rae.es/). It features direct message and inline search.
|
||||||
* [remoteTelegramShell](https://github.com/EnriqueMoran/remoteTelegramShell) by [EnriqueMoran](https://github.com/EnriqueMoran). Control your LinuxOS computer through Telegram.
|
* [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.
|
||||||
|
|
||||||
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**
|
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**
|
||||||
|
|
1
doc_req.txt
Normal file
1
doc_req.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pytelegrambotapi
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
51
docs/source/async_version/index.rst
Normal file
51
docs/source/async_version/index.rst
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
====================
|
||||||
|
AsyncTeleBot
|
||||||
|
====================
|
||||||
|
|
||||||
|
AsyncTeleBot methods
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.async_telebot
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Asyncio filters
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.asyncio_filters
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Asynchronous storage for states
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.asyncio_storage
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
asyncio_helper file
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.asyncio_helper
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Asyncio handler backends
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. automodule:: telebot.asyncio_handler_backends
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
13
docs/source/calldata.rst
Normal file
13
docs/source/calldata.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
=====================
|
||||||
|
Callback data factory
|
||||||
|
=====================
|
||||||
|
|
||||||
|
|
||||||
|
callback\_data file
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.callback_data
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
66
docs/source/conf.py
Normal file
66
docs/source/conf.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# This file only contains a selection of the most common options. For a full
|
||||||
|
# list see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = 'pyTelegramBotAPI'
|
||||||
|
copyright = '2022, coder2020official'
|
||||||
|
author = 'coder2020official'
|
||||||
|
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = '1.0'
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx_rtd_theme',
|
||||||
|
'sphinx.ext.autosectionlabel',
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
html_logo = 'logo.png'
|
||||||
|
html_theme_options = {
|
||||||
|
'logo_only': True,
|
||||||
|
'display_version': False,
|
||||||
|
}
|
63
docs/source/index.rst
Normal file
63
docs/source/index.rst
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
.. pyTelegramBotAPI documentation master file, created by
|
||||||
|
sphinx-quickstart on Fri Feb 18 20:58:37 2022.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
|
||||||
|
Welcome to pyTelegramBotAPI's documentation!
|
||||||
|
============================================
|
||||||
|
|
||||||
|
=======
|
||||||
|
TeleBot
|
||||||
|
=======
|
||||||
|
TeleBot is synchronous and asynchronous implementation of `Telegram Bot API <https://core.telegram.org/bots/api>`_.
|
||||||
|
|
||||||
|
Chats
|
||||||
|
-----
|
||||||
|
English chat: `Private chat <https://telegram.me/joinchat/Bn4ixj84FIZVkwhk2jag6A>`__
|
||||||
|
|
||||||
|
Russian chat: `@pytelegrambotapi_talks_ru <https://t.me/pytelegrambotapi_talks_ru>`__
|
||||||
|
|
||||||
|
News: `@pyTelegramBotAPI <https://t.me/pytelegrambotapi>`__
|
||||||
|
|
||||||
|
Pypi: `Pypi <https://pypi.org/project/pyTelegramBotAPI/>`__
|
||||||
|
|
||||||
|
Source: `Github repository <https://github.com/eternnoir/pyTelegramBotAPI>`__
|
||||||
|
|
||||||
|
Some features:
|
||||||
|
-------------
|
||||||
|
Easy to learn and use.
|
||||||
|
|
||||||
|
Easy to understand.
|
||||||
|
|
||||||
|
Both sync and async.
|
||||||
|
|
||||||
|
Examples on features.
|
||||||
|
|
||||||
|
States
|
||||||
|
|
||||||
|
And more...
|
||||||
|
|
||||||
|
Content
|
||||||
|
--------
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
install
|
||||||
|
quick_start
|
||||||
|
types
|
||||||
|
sync_version/index
|
||||||
|
async_version/index
|
||||||
|
calldata
|
||||||
|
util
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
|
41
docs/source/install.rst
Normal file
41
docs/source/install.rst
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
==================
|
||||||
|
Installation Guide
|
||||||
|
==================
|
||||||
|
|
||||||
|
|
||||||
|
:toctree
|
||||||
|
|
||||||
|
Using PIP
|
||||||
|
----------
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install pyTelegramBotAPI
|
||||||
|
|
||||||
|
Using pipenv
|
||||||
|
------------
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pipenv install pyTelegramBotAPI
|
||||||
|
|
||||||
|
By cloning repository
|
||||||
|
---------------------
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ git clone https://github.com/eternnoir/pyTelegramBotAPI.git
|
||||||
|
$ cd pyTelegramBotAPI
|
||||||
|
$ python setup.py install
|
||||||
|
|
||||||
|
Directly using pip
|
||||||
|
------------------
|
||||||
|
.. code-block:: bash
|
||||||
|
$ pip install git+https://github.com/eternnoir/pyTelegramBotAPI.git
|
||||||
|
|
||||||
|
It is generally recommended to use the first option.
|
||||||
|
|
||||||
|
While the API is production-ready, it is still under development and it has regular updates, do not forget to update it regularly by calling:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install pytelegrambotapi --upgrade
|
||||||
|
|
||||||
|
|
BIN
docs/source/logo.png
Normal file
BIN
docs/source/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
16
docs/source/quick_start.rst
Normal file
16
docs/source/quick_start.rst
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
===========
|
||||||
|
Quick start
|
||||||
|
===========
|
||||||
|
|
||||||
|
Synchronous TeleBot
|
||||||
|
-------------------
|
||||||
|
.. literalinclude:: ../../examples/echo_bot.py
|
||||||
|
:language: python
|
||||||
|
|
||||||
|
Asynchronous TeleBot
|
||||||
|
--------------------
|
||||||
|
.. literalinclude:: ../../examples/asynchronous_telebot/echo_bot.py
|
||||||
|
:language: python
|
||||||
|
|
||||||
|
|
43
docs/source/sync_version/index.rst
Normal file
43
docs/source/sync_version/index.rst
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
===============
|
||||||
|
TeleBot version
|
||||||
|
===============
|
||||||
|
|
||||||
|
TeleBot methods
|
||||||
|
---------------
|
||||||
|
.. automodule:: telebot
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
custom_filters file
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.custom_filters
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Synchronous storage for states
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.storage
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
handler_backends file
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.handler_backends
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
apihelper
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.apihelper
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
10
docs/source/types.rst
Normal file
10
docs/source/types.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
============
|
||||||
|
Types of API
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. automodule:: telebot.types
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
12
docs/source/util.rst
Normal file
12
docs/source/util.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
============
|
||||||
|
Utils
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
util file
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: telebot.util
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
|
@ -0,0 +1,26 @@
|
||||||
|
from telebot import types
|
||||||
|
from telebot.async_telebot import AsyncTeleBot
|
||||||
|
from telebot.asyncio_filters import AdvancedCustomFilter
|
||||||
|
from telebot.callback_data import CallbackData, CallbackDataFilter
|
||||||
|
|
||||||
|
calendar_factory = CallbackData("year", "month", prefix="calendar")
|
||||||
|
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarCallbackFilter(AdvancedCustomFilter):
|
||||||
|
key = 'calendar_config'
|
||||||
|
|
||||||
|
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
|
||||||
|
return config.check(query=call)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
|
||||||
|
key = 'calendar_zoom_config'
|
||||||
|
|
||||||
|
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
|
||||||
|
return config.check(query=call)
|
||||||
|
|
||||||
|
|
||||||
|
def bind_filters(bot: AsyncTeleBot):
|
||||||
|
bot.add_custom_filter(CalendarCallbackFilter())
|
||||||
|
bot.add_custom_filter(CalendarZoomCallbackFilter())
|
|
@ -0,0 +1,92 @@
|
||||||
|
import calendar
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from filters import calendar_factory, calendar_zoom
|
||||||
|
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
EMTPY_FIELD = '1'
|
||||||
|
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
|
||||||
|
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_calendar_days(year: int, month: int):
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=7)
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=date(year=year, month=month, day=1).strftime('%b %Y'),
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=day,
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
for day in WEEK_DAYS
|
||||||
|
])
|
||||||
|
|
||||||
|
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
|
||||||
|
week_buttons = []
|
||||||
|
for day in week:
|
||||||
|
day_name = ' '
|
||||||
|
if day == today.day and today.year == year and today.month == month:
|
||||||
|
day_name = '🔘'
|
||||||
|
elif day != 0:
|
||||||
|
day_name = str(day)
|
||||||
|
week_buttons.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=day_name,
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*week_buttons)
|
||||||
|
|
||||||
|
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
|
||||||
|
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
|
||||||
|
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Previous month',
|
||||||
|
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Zoom out',
|
||||||
|
callback_data=calendar_zoom.new(year=year)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Next month',
|
||||||
|
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def generate_calendar_months(year: int):
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=3)
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=date(year=year, month=1, day=1).strftime('Year %Y'),
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=month,
|
||||||
|
callback_data=calendar_factory.new(year=year, month=month_number)
|
||||||
|
)
|
||||||
|
for month_number, month in MONTHS
|
||||||
|
])
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Previous year',
|
||||||
|
callback_data=calendar_zoom.new(year=year - 1)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Next year',
|
||||||
|
callback_data=calendar_zoom.new(year=year + 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return keyboard
|
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This Example will show you an advanced usage of CallbackData.
|
||||||
|
In this example calendar was implemented
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from filters import calendar_factory, calendar_zoom, bind_filters
|
||||||
|
from keyboards import generate_calendar_days, generate_calendar_months, EMTPY_FIELD
|
||||||
|
from telebot import types
|
||||||
|
from telebot.async_telebot import AsyncTeleBot
|
||||||
|
|
||||||
|
API_TOKEN = ''
|
||||||
|
bot = AsyncTeleBot(API_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='start')
|
||||||
|
async def start_command_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id,
|
||||||
|
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
|
||||||
|
"\nPress /calendar to see it.")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='calendar')
|
||||||
|
async def calendar_command_handler(message: types.Message):
|
||||||
|
now = date.today()
|
||||||
|
await bot.send_message(message.chat.id, 'Calendar',
|
||||||
|
reply_markup=generate_calendar_days(year=now.year, month=now.month))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
|
||||||
|
async def calendar_action_handler(call: types.CallbackQuery):
|
||||||
|
callback_data: dict = calendar_factory.parse(callback_data=call.data)
|
||||||
|
year, month = int(callback_data['year']), int(callback_data['month'])
|
||||||
|
|
||||||
|
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
|
||||||
|
reply_markup=generate_calendar_days(year=year, month=month))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
|
||||||
|
async def calendar_zoom_out_handler(call: types.CallbackQuery):
|
||||||
|
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
|
||||||
|
year = int(callback_data.get('year'))
|
||||||
|
|
||||||
|
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
|
||||||
|
reply_markup=generate_calendar_months(year=year))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
|
||||||
|
async def callback_empty_field_handler(call: types.CallbackQuery):
|
||||||
|
await bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bind_filters(bot)
|
||||||
|
asyncio.run(bot.infinity_polling())
|
|
@ -0,0 +1,127 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This Example will show you usage of TextFilter
|
||||||
|
In this example you will see how to use TextFilter
|
||||||
|
with (message_handler, callback_query_handler, poll_handler)
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from telebot import types
|
||||||
|
from telebot.async_telebot import AsyncTeleBot
|
||||||
|
from telebot.asyncio_filters import TextMatchFilter, TextFilter, IsReplyFilter
|
||||||
|
|
||||||
|
bot = AsyncTeleBot("")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='hello'))
|
||||||
|
async def hello_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
|
||||||
|
async def hello_handler_ignore_case(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
|
||||||
|
async def contains_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
|
||||||
|
async def contains_handler_ignore_case(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
|
||||||
|
async def starts_with_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
|
||||||
|
async def starts_with_handler_ignore_case(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
|
||||||
|
async def ends_with_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
|
||||||
|
async def ends_with_handler_ignore_case(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='/callback'))
|
||||||
|
async def send_callback(message: types.Message):
|
||||||
|
keyboard = types.InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
|
||||||
|
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
|
||||||
|
async def callback_query_handler(call: types.CallbackQuery):
|
||||||
|
await bot.answer_callback_query(call.id, call.data, show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
|
||||||
|
async def callback_query_handler_ignore_case(call: types.CallbackQuery):
|
||||||
|
await bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='/poll'))
|
||||||
|
async def send_poll(message: types.Message):
|
||||||
|
await bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
|
||||||
|
await bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
|
||||||
|
|
||||||
|
|
||||||
|
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
|
||||||
|
async def poll_question_handler(poll: types.Poll):
|
||||||
|
print(poll.question)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
|
||||||
|
async def poll_question_handler_ignore_case(poll: types.Poll):
|
||||||
|
print(poll.question + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
# either hi or contains one of (привет, salom)
|
||||||
|
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
|
||||||
|
async def multiple_patterns_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
|
||||||
|
async def multiple_starts_with_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
|
||||||
|
async def multiple_ends_with_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# !ban /ban .ban !бан /бан .бан
|
||||||
|
@bot.message_handler(is_reply=True,
|
||||||
|
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
|
||||||
|
async def ban_command_handler(message: types.Message):
|
||||||
|
if len(message.text) == 4 and message.chat.type != 'private':
|
||||||
|
try:
|
||||||
|
await bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
|
||||||
|
await bot.reply_to(message.reply_to_message, 'Banned.')
|
||||||
|
except Exception as err:
|
||||||
|
print(err.args)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot.add_custom_filter(TextMatchFilter())
|
||||||
|
bot.add_custom_filter(IsReplyFilter())
|
||||||
|
asyncio.run(bot.polling())
|
|
@ -0,0 +1,120 @@
|
||||||
|
import contextvars
|
||||||
|
import gettext
|
||||||
|
import os
|
||||||
|
|
||||||
|
from telebot.asyncio_handler_backends import BaseMiddleware
|
||||||
|
|
||||||
|
try:
|
||||||
|
from babel.support import LazyProxy
|
||||||
|
|
||||||
|
babel_imported = True
|
||||||
|
except ImportError:
|
||||||
|
babel_imported = False
|
||||||
|
|
||||||
|
|
||||||
|
class I18N(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
This middleware provides high-level tool for internationalization
|
||||||
|
It is based on gettext util.
|
||||||
|
"""
|
||||||
|
|
||||||
|
context_lang = contextvars.ContextVar('language', default=None)
|
||||||
|
|
||||||
|
def __init__(self, translations_path, domain_name: str):
|
||||||
|
super().__init__()
|
||||||
|
self.update_types = self.process_update_types()
|
||||||
|
|
||||||
|
self.path = translations_path
|
||||||
|
self.domain = domain_name
|
||||||
|
self.translations = self.find_translations()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_translations(self):
|
||||||
|
return list(self.translations)
|
||||||
|
|
||||||
|
def gettext(self, text: str, lang: str = None):
|
||||||
|
"""
|
||||||
|
Singular translations
|
||||||
|
"""
|
||||||
|
|
||||||
|
if lang is None:
|
||||||
|
lang = self.context_lang.get()
|
||||||
|
|
||||||
|
if lang not in self.translations:
|
||||||
|
return text
|
||||||
|
|
||||||
|
translator = self.translations[lang]
|
||||||
|
return translator.gettext(text)
|
||||||
|
|
||||||
|
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||||
|
"""
|
||||||
|
Plural translations
|
||||||
|
"""
|
||||||
|
if lang is None:
|
||||||
|
lang = self.context_lang.get()
|
||||||
|
|
||||||
|
if lang not in self.translations:
|
||||||
|
if n == 1:
|
||||||
|
return singular
|
||||||
|
return plural
|
||||||
|
|
||||||
|
translator = self.translations[lang]
|
||||||
|
return translator.ngettext(singular, plural, n)
|
||||||
|
|
||||||
|
def lazy_gettext(self, text: str, lang: str = None):
|
||||||
|
if not babel_imported:
|
||||||
|
raise RuntimeError('babel module is not imported. Check that you installed it.')
|
||||||
|
return LazyProxy(self.gettext, text, lang, enable_cache=False)
|
||||||
|
|
||||||
|
def lazy_ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||||
|
if not babel_imported:
|
||||||
|
raise RuntimeError('babel module is not imported. Check that you installed it.')
|
||||||
|
return LazyProxy(self.ngettext, singular, plural, lang, n, enable_cache=False)
|
||||||
|
|
||||||
|
async def get_user_language(self, obj):
|
||||||
|
"""
|
||||||
|
You need to override this method and return user language
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def process_update_types(self) -> list:
|
||||||
|
"""
|
||||||
|
You need to override this method and return any update types which you want to be processed
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def pre_process(self, message, data):
|
||||||
|
"""
|
||||||
|
context language variable will be set each time when update from 'process_update_types' comes
|
||||||
|
value is the result of 'get_user_language' method
|
||||||
|
"""
|
||||||
|
self.context_lang.set(await self.get_user_language(obj=message))
|
||||||
|
|
||||||
|
async def post_process(self, message, data, exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def find_translations(self):
|
||||||
|
"""
|
||||||
|
Looks for translations with passed 'domain' in passed 'path'
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for name in os.listdir(self.path):
|
||||||
|
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
|
||||||
|
|
||||||
|
if not os.path.isdir(translations_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
po_file = os.path.join(translations_path, self.domain + '.po')
|
||||||
|
mo_file = po_file[:-2] + 'mo'
|
||||||
|
|
||||||
|
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
|
||||||
|
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
|
||||||
|
|
||||||
|
with open(mo_file, 'rb') as file:
|
||||||
|
result[name] = gettext.GNUTranslations(file)
|
||||||
|
|
||||||
|
return result
|
|
@ -0,0 +1,34 @@
|
||||||
|
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
|
||||||
|
|
||||||
|
|
||||||
|
def languages_keyboard():
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="English", callback_data='en'),
|
||||||
|
InlineKeyboardButton(text="Русский", callback_data='ru'),
|
||||||
|
InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn')
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clicker_keyboard(_):
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text=_("click"), callback_data='click'),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def menu_keyboard(_):
|
||||||
|
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton(text=_("My user id")),
|
||||||
|
KeyboardButton(text=_("My user name")),
|
||||||
|
KeyboardButton(text=_("My first name"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
|
@ -0,0 +1,81 @@
|
||||||
|
# English translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Language-Team: en <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: keyboards.py:29
|
||||||
|
msgid "My user id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: keyboards.py:30
|
||||||
|
msgid "My user name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: keyboards.py:31
|
||||||
|
msgid "My first name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:97
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example\n"
|
||||||
|
"/menu - text menu example"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:121
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:130 main.py:150
|
||||||
|
#, fuzzy
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: main.py:135 main.py:155
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:163
|
||||||
|
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:203
|
||||||
|
msgid "Seems you confused language"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#~ msgid ""
|
||||||
|
#~ "Hello, {user_fist_name}!\n"
|
||||||
|
#~ "This is the example of multilanguage bot.\n"
|
||||||
|
#~ "Available commands:\n"
|
||||||
|
#~ "\n"
|
||||||
|
#~ "/lang - change your language\n"
|
||||||
|
#~ "/plural - pluralization example"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Russian translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"Language-Team: ru <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||||
|
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr "Клик"
|
||||||
|
|
||||||
|
#: keyboards.py:29
|
||||||
|
msgid "My user id"
|
||||||
|
msgstr "Мой user id"
|
||||||
|
|
||||||
|
#: keyboards.py:30
|
||||||
|
msgid "My user name"
|
||||||
|
msgstr "Мой user name"
|
||||||
|
|
||||||
|
#: keyboards.py:31
|
||||||
|
msgid "My first name"
|
||||||
|
msgstr "Мой first name"
|
||||||
|
|
||||||
|
#: main.py:97
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example\n"
|
||||||
|
"/menu - text menu example"
|
||||||
|
msgstr ""
|
||||||
|
"Привет, {user_fist_name}!\n"
|
||||||
|
"Это пример мультиязычного бота.\n"
|
||||||
|
"Доступные команды:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - изменить язык\n"
|
||||||
|
"/plural - пример плюрализации\n"
|
||||||
|
"/menu - Пример текстового меню"
|
||||||
|
|
||||||
|
#: main.py:121
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr "Язык был сменён"
|
||||||
|
|
||||||
|
#: main.py:130 main.py:150
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] "У вас {number} клик"
|
||||||
|
msgstr[1] "У вас {number} клика"
|
||||||
|
msgstr[2] "У вас {number} кликов"
|
||||||
|
|
||||||
|
#: main.py:135 main.py:155
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
"Это кликер.\n"
|
||||||
|
"\n"
|
||||||
|
|
||||||
|
#: main.py:163
|
||||||
|
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
|
||||||
|
msgstr "Это пример ReplyKeyboardMarkup меню в мультиязычном боте."
|
||||||
|
|
||||||
|
#: main.py:203
|
||||||
|
msgid "Seems you confused language"
|
||||||
|
msgstr "Кажется, вы перепутали язык"
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Uzbek (Latin) translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: uz_Latn\n"
|
||||||
|
"Language-Team: uz_Latn <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr "clik"
|
||||||
|
|
||||||
|
#: keyboards.py:29
|
||||||
|
msgid "My user id"
|
||||||
|
msgstr "Mani user id"
|
||||||
|
|
||||||
|
#: keyboards.py:30
|
||||||
|
msgid "My user name"
|
||||||
|
msgstr "Mani user name"
|
||||||
|
|
||||||
|
#: keyboards.py:31
|
||||||
|
msgid "My first name"
|
||||||
|
msgstr "Mani first name"
|
||||||
|
|
||||||
|
#: main.py:97
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example\n"
|
||||||
|
"/menu - text menu example"
|
||||||
|
msgstr ""
|
||||||
|
"Salom, {user_fist_name}!\n"
|
||||||
|
"Bu multilanguage bot misoli.\n"
|
||||||
|
"Mavjud buyruqlar:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - tilni ozgartirish\n"
|
||||||
|
"/plural - pluralizatsiya misoli\n"
|
||||||
|
"/menu - text menu misoli"
|
||||||
|
|
||||||
|
#: main.py:121
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr "Til ozgartirildi"
|
||||||
|
|
||||||
|
#: main.py:130 main.py:150
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] "Sizda {number}ta clik"
|
||||||
|
msgstr[1] "Sizda {number}ta clik"
|
||||||
|
|
||||||
|
#: main.py:135 main.py:155
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
"Bu clicker.\n"
|
||||||
|
"\n"
|
||||||
|
|
||||||
|
#: main.py:163
|
||||||
|
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
|
||||||
|
msgstr "Bu multilanguage bot da replykeyboardmarkup menyu misoli."
|
||||||
|
|
||||||
|
#: main.py:203
|
||||||
|
msgid "Seems you confused language"
|
||||||
|
msgstr "Tilni adashtirdiz"
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
"""
|
||||||
|
In this example you will learn how to adapt your bot to different languages
|
||||||
|
Using built-in middleware I18N.
|
||||||
|
|
||||||
|
You need to install babel package 'https://pypi.org/project/Babel/'
|
||||||
|
Babel provides a command-line interface for working with message catalogs
|
||||||
|
After installing babel package you have a script called 'pybabel'
|
||||||
|
Too see all the commands open terminal and type 'pybabel --help'
|
||||||
|
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
|
||||||
|
|
||||||
|
Create a directory 'locales' where our translations will be stored
|
||||||
|
|
||||||
|
First we need to extract texts:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot --input-dirs .
|
||||||
|
{domain_name}.pot - is the file where all translations are saved
|
||||||
|
The name of this file should be the same as domain which you pass to I18N class
|
||||||
|
In this example domain_name will be 'messages'
|
||||||
|
|
||||||
|
For gettext (singular texts) we use '_' alias and it works perfect
|
||||||
|
You may also you some alias for ngettext (plural texts) but you can face with a problem that
|
||||||
|
your plural texts are not being extracted
|
||||||
|
That is because by default 'pybabel extract' recognizes the following keywords:
|
||||||
|
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
|
||||||
|
To add your own keyword you can use '-k' flag
|
||||||
|
In this example for 'ngettext' i will assign double underscore alias '__'
|
||||||
|
|
||||||
|
Full command with pluralization support will look so:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||||
|
|
||||||
|
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l en
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l ru
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
|
||||||
|
|
||||||
|
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
|
||||||
|
After you translated all the texts you need to compile .po files:
|
||||||
|
pybabel compile -d locales
|
||||||
|
|
||||||
|
When you delete/update your texts you also need to update them in .po files:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||||
|
pybabel update -i locales/{domain_name}.pot -d locales
|
||||||
|
- translate
|
||||||
|
pybabel compile -d locales
|
||||||
|
|
||||||
|
If you have any exceptions check:
|
||||||
|
- you have installed babel
|
||||||
|
- translations are ready, so you just compiled it
|
||||||
|
- in the commands above you replaced {domain_name} to messages
|
||||||
|
- you are writing commands from correct path in terminal
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import keyboards
|
||||||
|
from telebot import types
|
||||||
|
from telebot.async_telebot import AsyncTeleBot
|
||||||
|
from telebot.asyncio_filters import TextMatchFilter, TextFilter
|
||||||
|
from i18n_base_midddleware import I18N
|
||||||
|
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
|
||||||
|
|
||||||
|
|
||||||
|
class I18NMiddleware(I18N):
|
||||||
|
|
||||||
|
def process_update_types(self) -> list:
|
||||||
|
"""
|
||||||
|
Here you need to return a list of update types which you want to be processed
|
||||||
|
"""
|
||||||
|
return ['message', 'callback_query']
|
||||||
|
|
||||||
|
async def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]):
|
||||||
|
"""
|
||||||
|
This method is called when new update comes (only updates which you return in 'process_update_types' method)
|
||||||
|
Returned language will be used in 'pre_process' method of parent class
|
||||||
|
Returned language will be set to context language variable.
|
||||||
|
If you need to get translation with user's actual language you don't have to pass it manually
|
||||||
|
It will be automatically passed from context language value.
|
||||||
|
However if you need some other language you can always pass it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_id = obj.from_user.id
|
||||||
|
|
||||||
|
if user_id not in users_lang:
|
||||||
|
users_lang[user_id] = 'en'
|
||||||
|
|
||||||
|
return users_lang[user_id]
|
||||||
|
|
||||||
|
|
||||||
|
storage = StateMemoryStorage()
|
||||||
|
bot = AsyncTeleBot("", state_storage=storage)
|
||||||
|
|
||||||
|
i18n = I18NMiddleware(translations_path='locales', domain_name='messages')
|
||||||
|
_ = i18n.gettext # for singular translations
|
||||||
|
__ = i18n.ngettext # for plural translations
|
||||||
|
|
||||||
|
# These are example storages, do not use it in a production development
|
||||||
|
users_lang = {}
|
||||||
|
users_clicks = {}
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='start')
|
||||||
|
async def start_handler(message: types.Message):
|
||||||
|
text = _("Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example\n"
|
||||||
|
"/menu - text menu example")
|
||||||
|
|
||||||
|
# remember don't use f string for interpolation, use .format method instead
|
||||||
|
text = text.format(user_fist_name=message.from_user.first_name)
|
||||||
|
await bot.send_message(message.from_user.id, text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='lang')
|
||||||
|
async def change_language_handler(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
|
||||||
|
reply_markup=keyboards.languages_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn']))
|
||||||
|
async def language_handler(call: types.CallbackQuery):
|
||||||
|
lang = call.data
|
||||||
|
users_lang[call.from_user.id] = lang
|
||||||
|
|
||||||
|
# When you changed user language, you have to pass it manually beacause it is not changed in context
|
||||||
|
await bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='plural')
|
||||||
|
async def pluralization_handler(message: types.Message):
|
||||||
|
if not users_clicks.get(message.from_user.id):
|
||||||
|
users_clicks[message.from_user.id] = 0
|
||||||
|
clicks = users_clicks[message.from_user.id]
|
||||||
|
|
||||||
|
text = __(
|
||||||
|
singular="You have {number} click",
|
||||||
|
plural="You have {number} clicks",
|
||||||
|
n=clicks
|
||||||
|
)
|
||||||
|
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||||
|
|
||||||
|
await bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
|
||||||
|
async def click_handler(call: types.CallbackQuery):
|
||||||
|
if not users_clicks.get(call.from_user.id):
|
||||||
|
users_clicks[call.from_user.id] = 1
|
||||||
|
else:
|
||||||
|
users_clicks[call.from_user.id] += 1
|
||||||
|
|
||||||
|
clicks = users_clicks[call.from_user.id]
|
||||||
|
|
||||||
|
text = __(
|
||||||
|
singular="You have {number} click",
|
||||||
|
plural="You have {number} clicks",
|
||||||
|
n=clicks
|
||||||
|
)
|
||||||
|
text = _("This is clicker.\n\n") + text.format(number=clicks)
|
||||||
|
|
||||||
|
await bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||||
|
reply_markup=keyboards.clicker_keyboard(_))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='menu')
|
||||||
|
async def menu_handler(message: types.Message):
|
||||||
|
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
|
||||||
|
await bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_))
|
||||||
|
|
||||||
|
|
||||||
|
# For lazy tranlations
|
||||||
|
# lazy gettext is used when you don't know user's locale
|
||||||
|
# It can be used for example to handle text buttons in multilanguage bot
|
||||||
|
# The actual translation will be delayed until update comes and context language is set
|
||||||
|
l_ = i18n.lazy_gettext
|
||||||
|
|
||||||
|
|
||||||
|
# Handlers below will handle text according to user's language
|
||||||
|
@bot.message_handler(text=l_("My user id"))
|
||||||
|
async def return_user_id(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, str(message.from_user.id))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=l_("My user name"))
|
||||||
|
async def return_user_id(message: types.Message):
|
||||||
|
username = message.from_user.username
|
||||||
|
if not username:
|
||||||
|
username = '-'
|
||||||
|
await bot.send_message(message.chat.id, username)
|
||||||
|
|
||||||
|
|
||||||
|
# You can make it case insensitive
|
||||||
|
@bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True))
|
||||||
|
async def return_user_id(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.from_user.first_name)
|
||||||
|
|
||||||
|
|
||||||
|
all_menu_texts = []
|
||||||
|
for language in i18n.available_translations:
|
||||||
|
for menu_text in ("My user id", "My user name", "My first name"):
|
||||||
|
all_menu_texts.append(_(menu_text, language))
|
||||||
|
|
||||||
|
|
||||||
|
# When user confused language. (handles all menu buttons texts)
|
||||||
|
@bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True))
|
||||||
|
async def missed_message(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot.setup_middleware(i18n)
|
||||||
|
bot.add_custom_filter(TextMatchFilter())
|
||||||
|
asyncio.run(bot.infinity_polling())
|
|
@ -0,0 +1,25 @@
|
||||||
|
import telebot
|
||||||
|
from telebot import types, AdvancedCustomFilter
|
||||||
|
from telebot.callback_data import CallbackData, CallbackDataFilter
|
||||||
|
|
||||||
|
calendar_factory = CallbackData("year", "month", prefix="calendar")
|
||||||
|
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarCallbackFilter(AdvancedCustomFilter):
|
||||||
|
key = 'calendar_config'
|
||||||
|
|
||||||
|
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
|
||||||
|
return config.check(query=call)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
|
||||||
|
key = 'calendar_zoom_config'
|
||||||
|
|
||||||
|
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
|
||||||
|
return config.check(query=call)
|
||||||
|
|
||||||
|
|
||||||
|
def bind_filters(bot: telebot.TeleBot):
|
||||||
|
bot.add_custom_filter(CalendarCallbackFilter())
|
||||||
|
bot.add_custom_filter(CalendarZoomCallbackFilter())
|
|
@ -0,0 +1,92 @@
|
||||||
|
import calendar
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from examples.callback_data_examples.advanced_calendar_example.filters import calendar_factory, calendar_zoom
|
||||||
|
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
EMTPY_FIELD = '1'
|
||||||
|
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
|
||||||
|
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_calendar_days(year: int, month: int):
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=7)
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=date(year=year, month=month, day=1).strftime('%b %Y'),
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=day,
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
for day in WEEK_DAYS
|
||||||
|
])
|
||||||
|
|
||||||
|
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
|
||||||
|
week_buttons = []
|
||||||
|
for day in week:
|
||||||
|
day_name = ' '
|
||||||
|
if day == today.day and today.year == year and today.month == month:
|
||||||
|
day_name = '🔘'
|
||||||
|
elif day != 0:
|
||||||
|
day_name = str(day)
|
||||||
|
week_buttons.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=day_name,
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*week_buttons)
|
||||||
|
|
||||||
|
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
|
||||||
|
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
|
||||||
|
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Previous month',
|
||||||
|
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Zoom out',
|
||||||
|
callback_data=calendar_zoom.new(year=year)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Next month',
|
||||||
|
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def generate_calendar_months(year: int):
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=3)
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=date(year=year, month=1, day=1).strftime('Year %Y'),
|
||||||
|
callback_data=EMTPY_FIELD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard.add(*[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=month,
|
||||||
|
callback_data=calendar_factory.new(year=year, month=month_number)
|
||||||
|
)
|
||||||
|
for month_number, month in MONTHS
|
||||||
|
])
|
||||||
|
keyboard.add(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Previous year',
|
||||||
|
callback_data=calendar_zoom.new(year=year - 1)
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text='Next year',
|
||||||
|
callback_data=calendar_zoom.new(year=year + 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return keyboard
|
|
@ -0,0 +1,56 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This Example will show you an advanced usage of CallbackData.
|
||||||
|
In this example calendar was implemented
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from examples.callback_data_examples.advanced_calendar_example.keyboards import generate_calendar_days, \
|
||||||
|
generate_calendar_months, EMTPY_FIELD
|
||||||
|
from filters import calendar_factory, calendar_zoom, bind_filters
|
||||||
|
from telebot import types, TeleBot
|
||||||
|
|
||||||
|
API_TOKEN = ''
|
||||||
|
bot = TeleBot(API_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='start')
|
||||||
|
def start_command_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id,
|
||||||
|
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
|
||||||
|
"\nPress /calendar to see it.")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='calendar')
|
||||||
|
def calendar_command_handler(message: types.Message):
|
||||||
|
now = date.today()
|
||||||
|
bot.send_message(message.chat.id, 'Calendar', reply_markup=generate_calendar_days(year=now.year, month=now.month))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
|
||||||
|
def calendar_action_handler(call: types.CallbackQuery):
|
||||||
|
callback_data: dict = calendar_factory.parse(callback_data=call.data)
|
||||||
|
year, month = int(callback_data['year']), int(callback_data['month'])
|
||||||
|
|
||||||
|
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
|
||||||
|
reply_markup=generate_calendar_days(year=year, month=month))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
|
||||||
|
def calendar_zoom_out_handler(call: types.CallbackQuery):
|
||||||
|
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
|
||||||
|
year = int(callback_data.get('year'))
|
||||||
|
|
||||||
|
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
|
||||||
|
reply_markup=generate_calendar_months(year=year))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
|
||||||
|
def callback_empty_field_handler(call: types.CallbackQuery):
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bind_filters(bot)
|
||||||
|
bot.infinity_polling()
|
125
examples/custom_filters/advanced_text_filter.py
Normal file
125
examples/custom_filters/advanced_text_filter.py
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This Example will show you usage of TextFilter
|
||||||
|
In this example you will see how to use TextFilter
|
||||||
|
with (message_handler, callback_query_handler, poll_handler)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telebot import TeleBot, types
|
||||||
|
from telebot.custom_filters import TextFilter, TextMatchFilter, IsReplyFilter
|
||||||
|
|
||||||
|
bot = TeleBot("")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='hello'))
|
||||||
|
def hello_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
|
||||||
|
def hello_handler_ignore_case(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
|
||||||
|
def contains_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
|
||||||
|
def contains_handler_ignore_case(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
|
||||||
|
def starts_with_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
|
||||||
|
def starts_with_handler_ignore_case(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
|
||||||
|
def ends_with_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
|
||||||
|
def ends_with_handler_ignore_case(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='/callback'))
|
||||||
|
def send_callback(message: types.Message):
|
||||||
|
keyboard = types.InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
|
||||||
|
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
|
||||||
|
def callback_query_handler(call: types.CallbackQuery):
|
||||||
|
bot.answer_callback_query(call.id, call.data, show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
|
||||||
|
def callback_query_handler_ignore_case(call: types.CallbackQuery):
|
||||||
|
bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(text=TextFilter(equals='/poll'))
|
||||||
|
def send_poll(message: types.Message):
|
||||||
|
bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
|
||||||
|
bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
|
||||||
|
|
||||||
|
|
||||||
|
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
|
||||||
|
def poll_question_handler(poll: types.Poll):
|
||||||
|
print(poll.question)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
|
||||||
|
def poll_question_handler_ignore_case(poll: types.Poll):
|
||||||
|
print(poll.question + ' ignore case')
|
||||||
|
|
||||||
|
|
||||||
|
# either hi or contains one of (привет, salom)
|
||||||
|
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
|
||||||
|
def multiple_patterns_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
|
||||||
|
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
|
||||||
|
def multiple_starts_with_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
|
||||||
|
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
|
||||||
|
def multiple_ends_with_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
|
||||||
|
# !ban /ban .ban !бан /бан .бан
|
||||||
|
@bot.message_handler(is_reply=True,
|
||||||
|
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
|
||||||
|
def ban_command_handler(message: types.Message):
|
||||||
|
if len(message.text) == 4 and message.chat.type != 'private':
|
||||||
|
try:
|
||||||
|
bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
|
||||||
|
bot.reply_to(message.reply_to_message, 'Banned.')
|
||||||
|
except Exception as err:
|
||||||
|
print(err.args)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot.add_custom_filter(TextMatchFilter())
|
||||||
|
bot.add_custom_filter(IsReplyFilter())
|
||||||
|
bot.infinity_polling()
|
66
examples/i18n_class_example/i18n_class.py
Normal file
66
examples/i18n_class_example/i18n_class.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import gettext
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class I18N:
|
||||||
|
"""
|
||||||
|
This class provides high-level tool for internationalization
|
||||||
|
It is based on gettext util.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, translations_path, domain_name: str):
|
||||||
|
self.path = translations_path
|
||||||
|
self.domain = domain_name
|
||||||
|
self.translations = self.find_translations()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_translations(self):
|
||||||
|
return list(self.translations)
|
||||||
|
|
||||||
|
def gettext(self, text: str, lang: str = None):
|
||||||
|
"""
|
||||||
|
Singular translations
|
||||||
|
"""
|
||||||
|
if not lang or lang not in self.translations:
|
||||||
|
return text
|
||||||
|
|
||||||
|
translator = self.translations[lang]
|
||||||
|
return translator.gettext(text)
|
||||||
|
|
||||||
|
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
|
||||||
|
"""
|
||||||
|
Plural translations
|
||||||
|
"""
|
||||||
|
if not lang or lang not in self.translations:
|
||||||
|
if n == 1:
|
||||||
|
return singular
|
||||||
|
return plural
|
||||||
|
|
||||||
|
translator = self.translations[lang]
|
||||||
|
return translator.ngettext(singular, plural, n)
|
||||||
|
|
||||||
|
def find_translations(self):
|
||||||
|
"""
|
||||||
|
Looks for translations with passed 'domain' in passed 'path'
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for name in os.listdir(self.path):
|
||||||
|
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
|
||||||
|
|
||||||
|
if not os.path.isdir(translations_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
po_file = os.path.join(translations_path, self.domain + '.po')
|
||||||
|
mo_file = po_file[:-2] + 'mo'
|
||||||
|
|
||||||
|
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
|
||||||
|
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
|
||||||
|
|
||||||
|
with open(mo_file, 'rb') as file:
|
||||||
|
result[name] = gettext.GNUTranslations(file)
|
||||||
|
|
||||||
|
return result
|
23
examples/i18n_class_example/keyboards.py
Normal file
23
examples/i18n_class_example/keyboards.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
|
||||||
|
def languages_keyboard():
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="English", callback_data='en'),
|
||||||
|
InlineKeyboardButton(text="Русский", callback_data='ru'),
|
||||||
|
InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn')
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clicker_keyboard(_, lang):
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,51 @@
|
||||||
|
# English translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Language-Team: en <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:78
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:102
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: main.py:114
|
||||||
|
#, fuzzy
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: main.py:120
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Russian translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"Language-Team: ru <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||||
|
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr "Клик"
|
||||||
|
|
||||||
|
#: main.py:78
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example"
|
||||||
|
msgstr ""
|
||||||
|
"Привет, {user_fist_name}!\n"
|
||||||
|
"Это пример мультиязычного бота.\n"
|
||||||
|
"Доступные команды:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - изменить язык\n"
|
||||||
|
"/plural - пример плюрализации"
|
||||||
|
|
||||||
|
#: main.py:102
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr "Язык был сменён"
|
||||||
|
|
||||||
|
#: main.py:114
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] "У вас {number} клик"
|
||||||
|
msgstr[1] "У вас {number} клика"
|
||||||
|
msgstr[2] "У вас {number} кликов"
|
||||||
|
|
||||||
|
#: main.py:120
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
"Это кликер.\n"
|
||||||
|
"\n"
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Uzbek (Latin) translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
|
||||||
|
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: uz_Latn\n"
|
||||||
|
"Language-Team: uz_Latn <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.9.1\n"
|
||||||
|
|
||||||
|
#: keyboards.py:20
|
||||||
|
msgid "click"
|
||||||
|
msgstr "clik"
|
||||||
|
|
||||||
|
#: main.py:78
|
||||||
|
msgid ""
|
||||||
|
"Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example"
|
||||||
|
msgstr ""
|
||||||
|
"Salom, {user_fist_name}!\n"
|
||||||
|
"Bu multilanguage bot misoli.\n"
|
||||||
|
"Mavjud buyruqlar:\n"
|
||||||
|
"\n"
|
||||||
|
"/lang - tilni ozgartirish\n"
|
||||||
|
"/plural - pluralizatsiya misoli"
|
||||||
|
|
||||||
|
#: main.py:102
|
||||||
|
msgid "Language has been changed"
|
||||||
|
msgstr "Til ozgartirildi"
|
||||||
|
|
||||||
|
#: main.py:114
|
||||||
|
msgid "You have {number} click"
|
||||||
|
msgid_plural "You have {number} clicks"
|
||||||
|
msgstr[0] "Sizda {number}ta clik"
|
||||||
|
msgstr[1] "Sizda {number}ta clik"
|
||||||
|
|
||||||
|
#: main.py:120
|
||||||
|
msgid ""
|
||||||
|
"This is clicker.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
"Bu clicker.\n"
|
||||||
|
"\n"
|
||||||
|
|
153
examples/i18n_class_example/main.py
Normal file
153
examples/i18n_class_example/main.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
"""
|
||||||
|
In this example you will learn how to adapt your bot to different languages
|
||||||
|
Using built-in class I18N.
|
||||||
|
|
||||||
|
You need to install babel package 'https://pypi.org/project/Babel/'
|
||||||
|
Babel provides a command-line interface for working with message catalogs
|
||||||
|
After installing babel package you have a script called 'pybabel'
|
||||||
|
Too see all the commands open terminal and type 'pybabel --help'
|
||||||
|
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
|
||||||
|
|
||||||
|
Create a directory 'locales' where our translations will be stored
|
||||||
|
|
||||||
|
First we need to extract texts:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot --input-dirs .
|
||||||
|
{domain_name}.pot - is the file where all translations are saved
|
||||||
|
The name of this file should be the same as domain which you pass to I18N class
|
||||||
|
In this example domain_name will be 'messages'
|
||||||
|
|
||||||
|
For gettext (singular texts) we use '_' alias and it works perfect
|
||||||
|
You may also you some alias for ngettext (plural texts) but you can face with a problem that
|
||||||
|
your plural texts are not being extracted
|
||||||
|
That is because by default 'pybabel extract' recognizes the following keywords:
|
||||||
|
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
|
||||||
|
To add your own keyword you can use '-k' flag
|
||||||
|
In this example for 'ngettext' i will assign double underscore alias '__'
|
||||||
|
|
||||||
|
Full command with pluralization support will look so:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||||
|
|
||||||
|
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l en
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l ru
|
||||||
|
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
|
||||||
|
|
||||||
|
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
|
||||||
|
After you translated all the texts you need to compile .po files:
|
||||||
|
pybabel compile -d locales
|
||||||
|
|
||||||
|
When you delete/update your texts you also need to update them in .po files:
|
||||||
|
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
|
||||||
|
pybabel update -i locales/{domain_name}.pot -d locales
|
||||||
|
- translate
|
||||||
|
pybabel compile -d locales
|
||||||
|
|
||||||
|
If you have any exceptions check:
|
||||||
|
- you have installed babel
|
||||||
|
- translations are ready, so you just compiled it
|
||||||
|
- in the commands above you replaced {domain_name} to messages
|
||||||
|
- you are writing commands from correct path in terminal
|
||||||
|
"""
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
import keyboards
|
||||||
|
from telebot import TeleBot, types, custom_filters
|
||||||
|
from telebot.storage.memory_storage import StateMemoryStorage
|
||||||
|
from i18n_class import I18N
|
||||||
|
|
||||||
|
storage = StateMemoryStorage()
|
||||||
|
bot = TeleBot("", state_storage=storage)
|
||||||
|
|
||||||
|
i18n = I18N(translations_path='locales', domain_name='messages')
|
||||||
|
_ = i18n.gettext # for singular translations
|
||||||
|
__ = i18n.ngettext # for plural translations
|
||||||
|
|
||||||
|
# These are example storages, do not use it in a production development
|
||||||
|
users_lang = {}
|
||||||
|
users_clicks = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_language(func):
|
||||||
|
"""
|
||||||
|
This decorator will pass to your handler current user's language
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
obj = args[0]
|
||||||
|
kwargs.update(lang=users_lang.get(obj.from_user.id, 'en'))
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='start')
|
||||||
|
@get_user_language
|
||||||
|
def start_handler(message: types.Message, lang):
|
||||||
|
text = _("Hello, {user_fist_name}!\n"
|
||||||
|
"This is the example of multilanguage bot.\n"
|
||||||
|
"Available commands:\n\n"
|
||||||
|
"/lang - change your language\n"
|
||||||
|
"/plural - pluralization example", lang=lang)
|
||||||
|
|
||||||
|
# remember don't use f string for interpolation, use .format method instead
|
||||||
|
text = text.format(user_fist_name=message.from_user.first_name)
|
||||||
|
bot.send_message(message.from_user.id, text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='lang')
|
||||||
|
def change_language_handler(message: types.Message):
|
||||||
|
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
|
||||||
|
reply_markup=keyboards.languages_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(contains=['en', 'ru', 'uz_Latn']))
|
||||||
|
def language_handler(call: types.CallbackQuery):
|
||||||
|
lang = call.data
|
||||||
|
users_lang[call.from_user.id] = lang
|
||||||
|
|
||||||
|
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
|
||||||
|
bot.delete_state(call.from_user.id)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.message_handler(commands='plural')
|
||||||
|
@get_user_language
|
||||||
|
def pluralization_handler(message: types.Message, lang):
|
||||||
|
if not users_clicks.get(message.from_user.id):
|
||||||
|
users_clicks[message.from_user.id] = 0
|
||||||
|
clicks = users_clicks[message.from_user.id]
|
||||||
|
|
||||||
|
text = __(
|
||||||
|
singular="You have {number} click",
|
||||||
|
plural="You have {number} clicks",
|
||||||
|
n=clicks,
|
||||||
|
lang=lang
|
||||||
|
)
|
||||||
|
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
|
||||||
|
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_, lang))
|
||||||
|
|
||||||
|
|
||||||
|
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
|
||||||
|
@get_user_language
|
||||||
|
def click_handler(call: types.CallbackQuery, lang):
|
||||||
|
if not users_clicks.get(call.from_user.id):
|
||||||
|
users_clicks[call.from_user.id] = 1
|
||||||
|
else:
|
||||||
|
users_clicks[call.from_user.id] += 1
|
||||||
|
|
||||||
|
clicks = users_clicks[call.from_user.id]
|
||||||
|
|
||||||
|
text = __(
|
||||||
|
singular="You have {number} click",
|
||||||
|
plural="You have {number} clicks",
|
||||||
|
n=clicks,
|
||||||
|
lang=lang
|
||||||
|
)
|
||||||
|
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
|
||||||
|
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
|
||||||
|
reply_markup=keyboards.clicker_keyboard(_, lang))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot.add_custom_filter(custom_filters.TextMatchFilter())
|
||||||
|
bot.infinity_polling()
|
|
@ -32,7 +32,7 @@ def set_timer(message):
|
||||||
|
|
||||||
@bot.message_handler(commands=['unset'])
|
@bot.message_handler(commands=['unset'])
|
||||||
def unset_timer(message):
|
def unset_timer(message):
|
||||||
schedule.clean(message.chat.id)
|
schedule.clear(message.chat.id)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -2456,7 +2456,7 @@ class TeleBot:
|
||||||
:param emojis:
|
:param emojis:
|
||||||
:param png_sticker: Required if `tgs_sticker` is None
|
:param png_sticker: Required if `tgs_sticker` is None
|
||||||
:param tgs_sticker: Required if `png_sticker` is None
|
:param tgs_sticker: Required if `png_sticker` is None
|
||||||
:webm_sticker:
|
:param webm_sticker:
|
||||||
:param mask_position:
|
:param mask_position:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
@ -2741,6 +2741,16 @@ class TeleBot:
|
||||||
"""
|
"""
|
||||||
self.add_middleware_handler(callback, update_types)
|
self.add_middleware_handler(callback, update_types)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_commands_input(commands, method_name):
|
||||||
|
if not isinstance(commands, list) or not all(isinstance(item, str) for item in commands):
|
||||||
|
logger.error(f"{method_name}: Commands filter should be list of strings (commands), unknown type supplied to the 'commands' filter list. Not able to use the supplied type.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_regexp_input(regexp, method_name):
|
||||||
|
if not isinstance(regexp, str):
|
||||||
|
logger.error(f"{method_name}: Regexp filter should be string. Not able to use the supplied type.")
|
||||||
|
|
||||||
def message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs):
|
def message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Message handler decorator.
|
Message handler decorator.
|
||||||
|
@ -2783,10 +2793,16 @@ class TeleBot:
|
||||||
if content_types is None:
|
if content_types is None:
|
||||||
content_types = ["text"]
|
content_types = ["text"]
|
||||||
|
|
||||||
|
method_name = "message_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("message_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2824,10 +2840,16 @@ class TeleBot:
|
||||||
:param pass_bot: Pass TeleBot to handler.
|
:param pass_bot: Pass TeleBot to handler.
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
method_name = "register_message_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("register_message_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("register_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("register_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2856,10 +2878,16 @@ class TeleBot:
|
||||||
if content_types is None:
|
if content_types is None:
|
||||||
content_types = ["text"]
|
content_types = ["text"]
|
||||||
|
|
||||||
|
method_name = "edited_message_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("edited_message_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2897,10 +2925,16 @@ class TeleBot:
|
||||||
:param pass_bot: Pass TeleBot to handler.
|
:param pass_bot: Pass TeleBot to handler.
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
method_name = "register_edited_message_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("register_edited_message_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("register_edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("register_edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2929,10 +2963,16 @@ class TeleBot:
|
||||||
if content_types is None:
|
if content_types is None:
|
||||||
content_types = ["text"]
|
content_types = ["text"]
|
||||||
|
|
||||||
|
method_name = "channel_post_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2968,10 +3008,16 @@ class TeleBot:
|
||||||
:param pass_bot: Pass TeleBot to handler.
|
:param pass_bot: Pass TeleBot to handler.
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
method_name = "register_channel_post_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("register_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("register_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("register_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -2998,10 +3044,16 @@ class TeleBot:
|
||||||
if content_types is None:
|
if content_types is None:
|
||||||
content_types = ["text"]
|
content_types = ["text"]
|
||||||
|
|
||||||
|
method_name = "edited_channel_post_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
@ -3037,10 +3089,16 @@ class TeleBot:
|
||||||
:param pass_bot: Pass TeleBot to handler.
|
:param pass_bot: Pass TeleBot to handler.
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
method_name = "register_edited_channel_post_handler"
|
||||||
|
|
||||||
|
if commands is not None:
|
||||||
|
self.check_commands_input(commands, method_name)
|
||||||
if isinstance(commands, str):
|
if isinstance(commands, str):
|
||||||
logger.warning("register_edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
|
|
||||||
commands = [commands]
|
commands = [commands]
|
||||||
|
|
||||||
|
if regexp is not None:
|
||||||
|
self.check_regexp_input(regexp, method_name)
|
||||||
|
|
||||||
if isinstance(content_types, str):
|
if isinstance(content_types, str):
|
||||||
logger.warning("register_edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
logger.warning("register_edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
|
||||||
content_types = [content_types]
|
content_types = [content_types]
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from telebot import types
|
||||||
|
|
||||||
|
|
||||||
class SimpleCustomFilter(ABC):
|
class SimpleCustomFilter(ABC):
|
||||||
"""
|
"""
|
||||||
|
@ -30,6 +34,95 @@ class AdvancedCustomFilter(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TextFilter:
|
||||||
|
"""
|
||||||
|
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
|
||||||
|
|
||||||
|
example of usage is in examples/custom_filters/advanced_text_filter.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
equals: Optional[str] = None,
|
||||||
|
contains: Optional[Union[list, tuple]] = None,
|
||||||
|
starts_with: Optional[Union[str, list, tuple]] = None,
|
||||||
|
ends_with: Optional[Union[str, list, tuple]] = None,
|
||||||
|
ignore_case: bool = False):
|
||||||
|
|
||||||
|
"""
|
||||||
|
:param equals: string, True if object's text is equal to passed string
|
||||||
|
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
|
||||||
|
:param starts_with: string, True if object's text starts with passed string
|
||||||
|
:param ends_with: string, True if object's text starts with passed string
|
||||||
|
:param ignore_case: bool (default False), case insensitive
|
||||||
|
"""
|
||||||
|
|
||||||
|
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
|
||||||
|
if to_check == 0:
|
||||||
|
raise ValueError('None of the check modes was specified')
|
||||||
|
|
||||||
|
self.equals = equals
|
||||||
|
self.contains = self._check_iterable(contains, filter_name='contains')
|
||||||
|
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
|
||||||
|
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
|
||||||
|
self.ignore_case = ignore_case
|
||||||
|
|
||||||
|
def _check_iterable(self, iterable, filter_name):
|
||||||
|
if not iterable:
|
||||||
|
pass
|
||||||
|
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
|
||||||
|
raise ValueError(f"Incorrect value of {filter_name!r}")
|
||||||
|
elif isinstance(iterable, str):
|
||||||
|
iterable = [iterable]
|
||||||
|
elif isinstance(iterable, list) or isinstance(iterable, tuple):
|
||||||
|
iterable = [i for i in iterable if isinstance(i, str)]
|
||||||
|
return iterable
|
||||||
|
|
||||||
|
async def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
|
||||||
|
|
||||||
|
if isinstance(obj, types.Poll):
|
||||||
|
text = obj.question
|
||||||
|
elif isinstance(obj, types.Message):
|
||||||
|
text = obj.text or obj.caption
|
||||||
|
elif isinstance(obj, types.CallbackQuery):
|
||||||
|
text = obj.data
|
||||||
|
elif isinstance(obj, types.InlineQuery):
|
||||||
|
text = obj.query
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.ignore_case:
|
||||||
|
text = text.lower()
|
||||||
|
prepare_func = lambda string: str(string).lower()
|
||||||
|
else:
|
||||||
|
prepare_func = str
|
||||||
|
|
||||||
|
if self.equals:
|
||||||
|
result = prepare_func(self.equals) == text
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.contains:
|
||||||
|
result = any([prepare_func(i) in text for i in self.contains])
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not any((self.starts_with, self.ends_with)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.starts_with:
|
||||||
|
result = any([text.startswith(prepare_func(i)) for i in self.starts_with])
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not self.ends_with:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.ends_with:
|
||||||
|
return any([text.endswith(prepare_func(i)) for i in self.ends_with])
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TextMatchFilter(AdvancedCustomFilter):
|
class TextMatchFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check Text message.
|
Filter to check Text message.
|
||||||
|
@ -42,8 +135,13 @@ class TextMatchFilter(AdvancedCustomFilter):
|
||||||
key = 'text'
|
key = 'text'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
if type(text) is list:return message.text in text
|
if isinstance(text, TextFilter):
|
||||||
else: return text == message.text
|
return await text.check(message)
|
||||||
|
elif type(text) is list:
|
||||||
|
return message.text in text
|
||||||
|
else:
|
||||||
|
return text == message.text
|
||||||
|
|
||||||
|
|
||||||
class TextContainsFilter(AdvancedCustomFilter):
|
class TextContainsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -58,7 +156,15 @@ class TextContainsFilter(AdvancedCustomFilter):
|
||||||
key = 'text_contains'
|
key = 'text_contains'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
return text in message.text
|
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
|
||||||
|
raise ValueError("Incorrect text_contains value")
|
||||||
|
elif isinstance(text, str):
|
||||||
|
text = [text]
|
||||||
|
elif isinstance(text, list) or isinstance(text, tuple):
|
||||||
|
text = [i for i in text if isinstance(i, str)]
|
||||||
|
|
||||||
|
return any([i in message.text for i in text])
|
||||||
|
|
||||||
|
|
||||||
class TextStartsFilter(AdvancedCustomFilter):
|
class TextStartsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -70,9 +176,11 @@ class TextStartsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = 'text_startswith'
|
key = 'text_startswith'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
return message.text.startswith(text)
|
return message.text.startswith(text)
|
||||||
|
|
||||||
|
|
||||||
class ChatFilter(AdvancedCustomFilter):
|
class ChatFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether chat_id corresponds to given chat_id.
|
Check whether chat_id corresponds to given chat_id.
|
||||||
|
@ -82,9 +190,11 @@ class ChatFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = 'chat_id'
|
key = 'chat_id'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
return message.chat.id in text
|
return message.chat.id in text
|
||||||
|
|
||||||
|
|
||||||
class ForwardFilter(SimpleCustomFilter):
|
class ForwardFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether message was forwarded from channel or group.
|
Check whether message was forwarded from channel or group.
|
||||||
|
@ -99,6 +209,7 @@ class ForwardFilter(SimpleCustomFilter):
|
||||||
async def check(self, message):
|
async def check(self, message):
|
||||||
return message.forward_from_chat is not None
|
return message.forward_from_chat is not None
|
||||||
|
|
||||||
|
|
||||||
class IsReplyFilter(SimpleCustomFilter):
|
class IsReplyFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether message is a reply.
|
Check whether message is a reply.
|
||||||
|
@ -114,7 +225,6 @@ class IsReplyFilter(SimpleCustomFilter):
|
||||||
return message.reply_to_message is not None
|
return message.reply_to_message is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageFilter(AdvancedCustomFilter):
|
class LanguageFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check users language_code.
|
Check users language_code.
|
||||||
|
@ -127,8 +237,11 @@ class LanguageFilter(AdvancedCustomFilter):
|
||||||
key = 'language_code'
|
key = 'language_code'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
if type(text) is list:return message.from_user.language_code in text
|
if type(text) is list:
|
||||||
else: return message.from_user.language_code == text
|
return message.from_user.language_code in text
|
||||||
|
else:
|
||||||
|
return message.from_user.language_code == text
|
||||||
|
|
||||||
|
|
||||||
class IsAdminFilter(SimpleCustomFilter):
|
class IsAdminFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -147,6 +260,7 @@ class IsAdminFilter(SimpleCustomFilter):
|
||||||
result = await self._bot.get_chat_member(message.chat.id, message.from_user.id)
|
result = await self._bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||||
return result.status in ['creator', 'administrator']
|
return result.status in ['creator', 'administrator']
|
||||||
|
|
||||||
|
|
||||||
class StateFilter(AdvancedCustomFilter):
|
class StateFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check state.
|
Filter to check state.
|
||||||
|
@ -154,15 +268,20 @@ class StateFilter(AdvancedCustomFilter):
|
||||||
Example:
|
Example:
|
||||||
@bot.message_handler(state=1)
|
@bot.message_handler(state=1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
key = 'state'
|
key = 'state'
|
||||||
|
|
||||||
async def check(self, message, text):
|
async def check(self, message, text):
|
||||||
if text == '*': return True
|
if text == '*': return True
|
||||||
|
|
||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
new_text = [i.name for i in text]
|
new_text = []
|
||||||
|
for i in text:
|
||||||
|
if isclass(i): i = i.name
|
||||||
|
new_text.append(i)
|
||||||
text = new_text
|
text = new_text
|
||||||
elif isinstance(text, object):
|
elif isinstance(text, object):
|
||||||
text = text.name
|
text = text.name
|
||||||
|
@ -176,12 +295,13 @@ class StateFilter(AdvancedCustomFilter):
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
user_state = await self.bot.current_states.get_state(message.chat.id,message.from_user.id)
|
user_state = await self.bot.current_states.get_state(message.chat.id, message.from_user.id)
|
||||||
if user_state == text:
|
if user_state == text:
|
||||||
return True
|
return True
|
||||||
elif type(text) is list and user_state in text:
|
elif type(text) is list and user_state in text:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class IsDigitFilter(SimpleCustomFilter):
|
class IsDigitFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check whether the string is made up of only digits.
|
Filter to check whether the string is made up of only digits.
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
class BaseMiddleware:
|
class BaseMiddleware:
|
||||||
"""
|
"""
|
||||||
Base class for middleware.
|
Base class for middleware.
|
||||||
|
|
||||||
Your middlewares should be inherited from this class.
|
Your middlewares should be inherited from this class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def pre_process(self, message, data):
|
async def pre_process(self, message, data):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def post_process(self, message, data, exception):
|
async def post_process(self, message, data, exception):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ class BaseMiddleware:
|
||||||
class State:
|
class State:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.name = None
|
self.name = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from telebot import types
|
||||||
|
|
||||||
|
|
||||||
class SimpleCustomFilter(ABC):
|
class SimpleCustomFilter(ABC):
|
||||||
"""
|
"""
|
||||||
Simple Custom Filter base class.
|
Simple Custom Filter base class.
|
||||||
|
@ -29,6 +34,100 @@ class AdvancedCustomFilter(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TextFilter:
|
||||||
|
"""
|
||||||
|
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
|
||||||
|
|
||||||
|
example of usage is in examples/custom_filters/advanced_text_filter.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
equals: Optional[str] = None,
|
||||||
|
contains: Optional[Union[list, tuple]] = None,
|
||||||
|
starts_with: Optional[Union[str, list, tuple]] = None,
|
||||||
|
ends_with: Optional[Union[str, list, tuple]] = None,
|
||||||
|
ignore_case: bool = False):
|
||||||
|
|
||||||
|
"""
|
||||||
|
:param equals: string, True if object's text is equal to passed string
|
||||||
|
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
|
||||||
|
:param starts_with: string, True if object's text starts with passed string
|
||||||
|
:param ends_with: string, True if object's text starts with passed string
|
||||||
|
:param ignore_case: bool (default False), case insensitive
|
||||||
|
"""
|
||||||
|
|
||||||
|
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
|
||||||
|
if to_check == 0:
|
||||||
|
raise ValueError('None of the check modes was specified')
|
||||||
|
|
||||||
|
self.equals = equals
|
||||||
|
self.contains = self._check_iterable(contains, filter_name='contains')
|
||||||
|
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
|
||||||
|
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
|
||||||
|
self.ignore_case = ignore_case
|
||||||
|
|
||||||
|
def _check_iterable(self, iterable, filter_name: str):
|
||||||
|
if not iterable:
|
||||||
|
pass
|
||||||
|
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
|
||||||
|
raise ValueError(f"Incorrect value of {filter_name!r}")
|
||||||
|
elif isinstance(iterable, str):
|
||||||
|
iterable = [iterable]
|
||||||
|
elif isinstance(iterable, list) or isinstance(iterable, tuple):
|
||||||
|
iterable = [i for i in iterable if isinstance(i, str)]
|
||||||
|
return iterable
|
||||||
|
|
||||||
|
def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
|
||||||
|
|
||||||
|
if isinstance(obj, types.Poll):
|
||||||
|
text = obj.question
|
||||||
|
elif isinstance(obj, types.Message):
|
||||||
|
text = obj.text or obj.caption
|
||||||
|
elif isinstance(obj, types.CallbackQuery):
|
||||||
|
text = obj.data
|
||||||
|
elif isinstance(obj, types.InlineQuery):
|
||||||
|
text = obj.query
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.ignore_case:
|
||||||
|
text = text.lower()
|
||||||
|
|
||||||
|
if self.equals:
|
||||||
|
self.equals = self.equals.lower()
|
||||||
|
elif self.contains:
|
||||||
|
self.contains = tuple(map(str.lower, self.contains))
|
||||||
|
elif self.starts_with:
|
||||||
|
self.starts_with = tuple(map(str.lower, self.starts_with))
|
||||||
|
elif self.ends_with:
|
||||||
|
self.ends_with = tuple(map(str.lower, self.ends_with))
|
||||||
|
|
||||||
|
if self.equals:
|
||||||
|
result = self.equals == text
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.contains:
|
||||||
|
result = any([i in text for i in self.contains])
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not any((self.starts_with, self.ends_with)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.starts_with:
|
||||||
|
result = any([text.startswith(i) for i in self.starts_with])
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
elif not result and not self.ends_with:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.ends_with:
|
||||||
|
return any([text.endswith(i) for i in self.ends_with])
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
class TextMatchFilter(AdvancedCustomFilter):
|
class TextMatchFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check Text message.
|
Filter to check Text message.
|
||||||
|
@ -41,8 +140,13 @@ class TextMatchFilter(AdvancedCustomFilter):
|
||||||
key = 'text'
|
key = 'text'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
if type(text) is list:return message.text in text
|
if isinstance(text, TextFilter):
|
||||||
else: return text == message.text
|
return text.check(message)
|
||||||
|
elif type(text) is list:
|
||||||
|
return message.text in text
|
||||||
|
else:
|
||||||
|
return text == message.text
|
||||||
|
|
||||||
|
|
||||||
class TextContainsFilter(AdvancedCustomFilter):
|
class TextContainsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -57,7 +161,15 @@ class TextContainsFilter(AdvancedCustomFilter):
|
||||||
key = 'text_contains'
|
key = 'text_contains'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
return text in message.text
|
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
|
||||||
|
raise ValueError("Incorrect text_contains value")
|
||||||
|
elif isinstance(text, str):
|
||||||
|
text = [text]
|
||||||
|
elif isinstance(text, list) or isinstance(text, tuple):
|
||||||
|
text = [i for i in text if isinstance(i, str)]
|
||||||
|
|
||||||
|
return any([i in message.text for i in text])
|
||||||
|
|
||||||
|
|
||||||
class TextStartsFilter(AdvancedCustomFilter):
|
class TextStartsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -69,9 +181,11 @@ class TextStartsFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = 'text_startswith'
|
key = 'text_startswith'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
return message.text.startswith(text)
|
return message.text.startswith(text)
|
||||||
|
|
||||||
|
|
||||||
class ChatFilter(AdvancedCustomFilter):
|
class ChatFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether chat_id corresponds to given chat_id.
|
Check whether chat_id corresponds to given chat_id.
|
||||||
|
@ -81,9 +195,11 @@ class ChatFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = 'chat_id'
|
key = 'chat_id'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
return message.chat.id in text
|
return message.chat.id in text
|
||||||
|
|
||||||
|
|
||||||
class ForwardFilter(SimpleCustomFilter):
|
class ForwardFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether message was forwarded from channel or group.
|
Check whether message was forwarded from channel or group.
|
||||||
|
@ -98,6 +214,7 @@ class ForwardFilter(SimpleCustomFilter):
|
||||||
def check(self, message):
|
def check(self, message):
|
||||||
return message.forward_from_chat is not None
|
return message.forward_from_chat is not None
|
||||||
|
|
||||||
|
|
||||||
class IsReplyFilter(SimpleCustomFilter):
|
class IsReplyFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check whether message is a reply.
|
Check whether message is a reply.
|
||||||
|
@ -113,7 +230,6 @@ class IsReplyFilter(SimpleCustomFilter):
|
||||||
return message.reply_to_message is not None
|
return message.reply_to_message is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageFilter(AdvancedCustomFilter):
|
class LanguageFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Check users language_code.
|
Check users language_code.
|
||||||
|
@ -126,8 +242,11 @@ class LanguageFilter(AdvancedCustomFilter):
|
||||||
key = 'language_code'
|
key = 'language_code'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
if type(text) is list:return message.from_user.language_code in text
|
if type(text) is list:
|
||||||
else: return message.from_user.language_code == text
|
return message.from_user.language_code in text
|
||||||
|
else:
|
||||||
|
return message.from_user.language_code == text
|
||||||
|
|
||||||
|
|
||||||
class IsAdminFilter(SimpleCustomFilter):
|
class IsAdminFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
|
@ -145,6 +264,7 @@ class IsAdminFilter(SimpleCustomFilter):
|
||||||
def check(self, message):
|
def check(self, message):
|
||||||
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
|
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
|
||||||
|
|
||||||
|
|
||||||
class StateFilter(AdvancedCustomFilter):
|
class StateFilter(AdvancedCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check state.
|
Filter to check state.
|
||||||
|
@ -152,15 +272,20 @@ class StateFilter(AdvancedCustomFilter):
|
||||||
Example:
|
Example:
|
||||||
@bot.message_handler(state=1)
|
@bot.message_handler(state=1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
key = 'state'
|
key = 'state'
|
||||||
|
|
||||||
def check(self, message, text):
|
def check(self, message, text):
|
||||||
if text == '*': return True
|
if text == '*': return True
|
||||||
|
|
||||||
if isinstance(text, list):
|
if isinstance(text, list):
|
||||||
new_text = [i.name for i in text]
|
new_text = []
|
||||||
|
for i in text:
|
||||||
|
if isclass(i): i = i.name
|
||||||
|
new_text.append(i)
|
||||||
text = new_text
|
text = new_text
|
||||||
elif isinstance(text, object):
|
elif isinstance(text, object):
|
||||||
text = text.name
|
text = text.name
|
||||||
|
@ -173,11 +298,13 @@ class StateFilter(AdvancedCustomFilter):
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
user_state = self.bot.current_states.get_state(message.chat.id,message.from_user.id)
|
user_state = self.bot.current_states.get_state(message.chat.id, message.from_user.id)
|
||||||
if user_state == text:
|
if user_state == text:
|
||||||
return True
|
return True
|
||||||
elif type(text) is list and user_state in text:
|
elif type(text) is list and user_state in text:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class IsDigitFilter(SimpleCustomFilter):
|
class IsDigitFilter(SimpleCustomFilter):
|
||||||
"""
|
"""
|
||||||
Filter to check whether the string is made up of only digits.
|
Filter to check whether the string is made up of only digits.
|
||||||
|
|
|
@ -21,6 +21,7 @@ try:
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
pil_imported = True
|
pil_imported = True
|
||||||
except:
|
except:
|
||||||
pil_imported = False
|
pil_imported = False
|
||||||
|
@ -49,6 +50,7 @@ update_types = [
|
||||||
"my_chat_member", "chat_member", "chat_join_request"
|
"my_chat_member", "chat_member", "chat_join_request"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkerThread(threading.Thread):
|
class WorkerThread(threading.Thread):
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
@ -477,6 +479,7 @@ def webhook_google_functions(bot, request):
|
||||||
else:
|
else:
|
||||||
return 'Bot ON'
|
return 'Bot ON'
|
||||||
|
|
||||||
|
|
||||||
def antiflood(function, *args, **kwargs):
|
def antiflood(function, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Use this function inside loops in order to avoid getting TooManyRequests error.
|
Use this function inside loops in order to avoid getting TooManyRequests error.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user